From c1abc34c7ec42924904750b13b251dc03c0083ab Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Fri, 2 Jan 2026 16:40:18 +0000 Subject: [PATCH 1/8] Document codec testing roadmap Expand the user guide with custom FrameCodec guidance and example\nreferences.\n\nAdd codec testing tasks to the roadmap, update ADR-004 with\nimplementation guidance, and introduce a test observability ADR\nfor log and metrics capture. --- docs/adr-004-pluggable-protocol-codecs.md | 14 +++ docs/adr-006-test-observability.md | 110 ++++++++++++++++++++++ docs/roadmap.md | 14 +++ docs/users-guide.md | 45 +++++++++ 4 files changed, 183 insertions(+) create mode 100644 docs/adr-006-test-observability.md diff --git a/docs/adr-004-pluggable-protocol-codecs.md b/docs/adr-004-pluggable-protocol-codecs.md index 669fd0eb..24103f49 100644 --- a/docs/adr-004-pluggable-protocol-codecs.md +++ b/docs/adr-004-pluggable-protocol-codecs.md @@ -222,6 +222,18 @@ pub trait FrameCodec: Send + Sync + 'static { and explicit `try_from` conversions, suggesting a need for shared parsing helpers to reduce boilerplate. +## Implementation guidance + +- Add codec-specific tests that exercise encoder/decoder round-trips, payload + extraction, correlation identifiers, and maximum frame length enforcement. +- Use shared example codecs in `wireframe::codec::examples` to drive regression + and property-based tests without duplicating framing logic. +- Prefer `wireframe_testing` helpers that work with custom codecs, so test + harnesses do not assume length-delimited framing. +- Validate observability signals (logs, metrics, and protocol hooks) for codec + failures and recovery policies using the test observability harness once + available.[^adr-006] + ## Known Risks and Limitations - Associated types avoid RPITIT for `FrameCodec`, but the overall design still @@ -254,3 +266,5 @@ preserves existing behaviour through a default codec while enabling protocol specificity and reusability. By tying buffer sizing and maximum frame length to codec configuration, the design keeps transport-level constraints close to the framing rules that define them. + +[^adr-006]: See [adr-006-test-observability.md](adr-006-test-observability.md). diff --git a/docs/adr-006-test-observability.md b/docs/adr-006-test-observability.md new file mode 100644 index 00000000..956856d0 --- /dev/null +++ b/docs/adr-006-test-observability.md @@ -0,0 +1,110 @@ +# Architectural decision record (ADR) 006: test observability harness + +## Status + +Proposed. + +## Date + +2026-01-02. + +## Context and Problem Statement + +Wireframe relies on logs, metrics, and protocol hooks for runtime +observability. Upcoming codec hardening work adds structured errors and +recovery policies that must be verified alongside behaviour. Today, tests can +capture logs via `wireframe_testing::logger`, but metrics require ad hoc global +exporters, and there is no shared helper for asserting instrumentation. This +makes telemetry assertions inconsistent, brittle, and hard to reuse across +downstream crates. + +A dedicated test observability harness is needed so tests can assert on logging +and metrics deterministically without depending on external exporters or global +state leaks. + +## Decision Drivers + +- Provide deterministic, per-test observability assertions. +- Avoid external services or network dependencies in tests. +- Keep the harness reusable for downstream crates and example codecs. +- Guard global registries to prevent cross-test interference. + +## Requirements + +### Functional requirements + +- Capture logs and metrics for a single test run. +- Provide helper APIs for inspecting log records and metric samples. +- Support async tests and background tasks without losing telemetry. + +### Technical requirements + +- Avoid environment variable mutations in tests. +- Use in-process recorders and restore prior global state on drop. +- Keep dependencies minimal and aligned with existing crates. + +## Options Considered + +### Option A: keep ad hoc log capture (status quo) + +Continue using `logtest` directly in tests and rely on manual, ad hoc setup for +metrics. + +### Option B: add a unified test observability harness (preferred) + +Provide a `wireframe_testing::observability` module that installs a scoped log +capture and metrics recorder, returning a guard with inspection helpers. + +### Option C: rely on external exporters in integration tests + +Use Prometheus exporters or external telemetry services and parse their output +in tests. + +## Decision Outcome / Proposed Direction + +Adopt Option B and add a scoped test observability harness to +`wireframe_testing`. The harness should compose with the existing logger guard +and provide a metrics recorder suitable for assertions in unit, integration, +and behavioural tests. + +## Approach + +- Add an `ObservabilityHandle` that installs log capture and a metrics recorder + when constructed and restores previous globals on drop. +- Reuse `logtest` for logs and introduce a metrics recorder based on + `metrics-util` so tests can query counters and gauges without external + exporters. +- Provide helper methods to clear state between assertions and to filter by + labels when verifying counters. +- Serialise access with a global lock to avoid cross-test interference, and + document that tests using the handle should not run concurrently. + +## Consequences + +- Tests can assert on instrumentation for codec errors and recovery policies + without bespoke setup. +- Global recorder access requires serialisation, which may reduce parallelism + for observability-heavy test suites. +- The testing crate gains additional dependency surface for metrics capture. + +## Roadmap + +### Phase 1: Observability capture primitives + +- Step: Introduce the harness in `wireframe_testing`. + - Task: Add `ObservabilityHandle` and integrate with `LoggerHandle`. + - Task: Provide metric snapshot helpers for counters and gauges. + - Task: Document usage and thread-safety constraints. + +### Phase 2: Codec and recovery assertions + +- Step: Wire codec tests to the harness. + - Task: Add helpers for asserting codec error counters and log fields. + - Task: Update codec regression tests to use the new helpers. + +## Mitigation and rollback + +- Keep the harness behind a feature flag if global recorder conflicts with + downstream test environments. +- If metrics capture proves flaky, fall back to log-based assertions while + refining recorder isolation. diff --git a/docs/roadmap.md b/docs/roadmap.md index 331f9441..d1ff40d4 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -425,6 +425,18 @@ integration boundaries. - [ ] Measure fragmentation overhead versus unfragmented paths. - [ ] Record memory allocation baselines for payload wrapping and decoding. +### 9.7. Codec test harness and observability + +- [ ] 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. +- [ ] 9.7.3. Introduce a test observability harness in `wireframe_testing` that + captures logs and metrics per test run for asserting codec failures and + recovery policies.[^adr-006] +- [ ] 9.7.4. Add regression tests in `wireframe_testing` for the `CodecError` + taxonomy and recovery policy behaviours defined in 9.1.2. Requires 9.1.2. + ## 10. Wireframe client library foundation This phase delivers a first-class client runtime that mirrors the server's @@ -569,3 +581,5 @@ and usability. [message-versioning.md](message-versioning.md). [^adr-005]: See [adr-005-serializer-abstraction.md](adr-005-serializer-abstraction.md). +[^adr-006]: See +[adr-006-test-observability.md](adr-006-test-observability.md). diff --git a/docs/users-guide.md b/docs/users-guide.md index c7f4edb0..9e77df6e 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -117,6 +117,51 @@ helpers `send_response` and `send_response_framed` (or variants when encoding or I/O fails, and the connection closes after ten consecutive deserialization errors.[^6][^7] +### Custom frame codecs + +Custom protocols supply a `FrameCodec` implementation to describe their framing +rules. The codec owns the Tokio `Decoder` and `Encoder` types, while Wireframe +uses the trait surface to map frames to payload bytes and correlation data. + +A codec implementation must: + +- Define a `Frame` type and paired decoder/encoder implementations that return + `std::io::Error` on failure. +- Return only the logical payload bytes from `frame_payload` so metadata parsing + and deserialization run against the right buffer. +- Wrap outbound payloads with `wrap_payload`, adding any protocol headers or + metadata required by the wire format. +- Provide `correlation_id` when the protocol stores it outside the payload; + Wireframe only uses this hook when the deserialized envelope is missing a + correlation identifier. +- Report `max_frame_length`, which clamps inbound frames and seeds default + fragmentation limits. + +Install a custom codec with `with_codec`. The builder resets fragmentation to +the codec-derived defaults, so override fragmentation afterwards if the +protocol uses a different budget. When you already have a framed stream, use +`send_response_framed_with_codec` so responses pass through +`FrameCodec::wrap_payload`. + +Assume `MyCodec` implements `FrameCodec`: + +```rust,no_run +use std::sync::Arc; + +use wireframe::app::{Envelope, Handler, WireframeApp}; + +struct MyCodec; + +let handler: Handler = Arc::new(|_: &Envelope| Box::pin(async {})); + +let app = WireframeApp::new()? + .with_codec(MyCodec) + .route(1, handler)?; +``` + +See `examples/hotline_codec.rs` and `examples/mysql_codec.rs` for complete +implementations. + ## Packets, payloads, and serialization Packets drive routing. Implement the `Packet` trait (or use the bundled From 4087994a30277d2b1b63ec919d120bdbf317b223 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Sat, 3 Jan 2026 01:35:13 +0000 Subject: [PATCH 2/8] Update wireframe_testing design for codecs Align the testing crate design with pluggable FrameCodec support and the observability harness from ADR 006. Document codec-aware driver entry points, fixture expectations, and log/metrics capture behaviour so implementers can plan the work. --- docs/wireframe-testing-crate.md | 385 ++++++++++++++++++++------------ 1 file changed, 243 insertions(+), 142 deletions(-) diff --git a/docs/wireframe-testing-crate.md b/docs/wireframe-testing-crate.md index 6152384a..4146cc66 100644 --- a/docs/wireframe-testing-crate.md +++ b/docs/wireframe-testing-crate.md @@ -1,188 +1,289 @@ -# `wireframe_testing`: Testing Helpers for Wireframe +# `wireframe_testing`: testing helpers for Wireframe -`wireframe_testing` is a proposed companion crate providing utilities for unit -and integration tests. It focuses on driving `WireframeApp` instances with raw -frames, enabling fast tests without opening real network connections. +`wireframe_testing` is the companion crate for exercising Wireframe +applications and codecs in unit, integration, and behavioural tests. It +provides in-memory drivers for `WireframeApp` instances, codec-aware framing +helpers, and a scoped observability harness for log and metrics assertions. ## Motivation -The existing tests in [`tests/`](../tests) use a helper function `run_app` to -feed length-prefixed frames through an in-memory duplex stream. This helper -simplifies testing handlers by allowing assertions on encoded responses without -spinning up a full server. Encapsulating this logic in a dedicated crate keeps -test code concise and reusable across projects. +Wireframe now supports pluggable protocol codecs (ADR 004). The test harness +must encode and decode frames using the selected `FrameCodec`, preserving +protocol metadata and correlation identifiers. It must also allow malformed +wire bytes for negative codec tests. ADR 006 proposes a unified test +observability harness so tests can assert on logs and metrics deterministically +without external exporters. -## Crate Layout +## Design goals -- `wireframe_testing` - - `Cargo.toml` enabling the `tokio` and `rstest` dependencies used by the - helpers. - - `src/lib.rs` exposing asynchronous functions for driving apps with raw +- Work with any `FrameCodec`, including codecs that carry metadata and + correlation identifiers. +- Preserve frame metadata when driving handlers or asserting responses. +- Allow raw byte injection for malformed frame and recovery tests. +- Provide per-test log and metrics capture without leaking global state. +- Keep helpers fast by using in-memory duplex streams instead of sockets. -```frames. +## Crate layout + +- `src/lib.rs` re-exports the public API. +- `src/helpers.rs` provides in-memory drivers and codec-aware encode/decode + helpers. +- `src/observability.rs` implements the observability harness from ADR 006. +- `src/logging.rs` retains the standalone `LoggerHandle` fixture. +- `src/fixtures/` (or `src/codec_fixtures.rs`) stores reusable frame fixtures + for the default codec and example codecs. +- `src/multi_packet.rs` keeps the `collect_multi_packet` helper. + +## Dependencies + +```toml [dependencies] -tokio = { version = "1", features = ["macros", "rt"] } +tokio = { version = "1", features = ["macros", "rt", "io-util"] } +wireframe = { version = "0.1.0", path = ".." } +bincode = "2.0" +bytes = "1.0" +futures = "0.3" +tokio-util = { version = "0.7", features = ["codec"] } +log = "0.4" +logtest = "2" +metrics = "0.24.2" +metrics-util = "0.20.0" +rstest = "0.18.2" +``` + +## Codec-aware drivers -[dev-dependencies] -rstest = "0.18" +The helpers remain centred on a single in-memory driver that runs +`WireframeApp::handle_connection` against a `tokio::io::duplex` stream. The +driver is responsible for framing inbound and outbound data using the selected +`FrameCodec` and for surfacing server panics as `io::Error` values prefixed +with `server task failed`. + +`wireframe_testing` retains the `TestSerializer` trait alias to keep bounds +readable: + +```rust,no_run +use wireframe::app::Envelope; +use wireframe::frame::FrameMetadata; +use wireframe::serializer::Serializer; + +pub trait TestSerializer: + Serializer + FrameMetadata + Send + Sync + 'static +{ +} ``` -```rust -// src/lib.rs -pub mod helpers; - -pub use helpers::{ - drive_with_frame, - drive_with_frames, - drive_with_frame_with_capacity, - drive_with_frames_with_capacity, - drive_with_bincode, -}; +### Driver entry points + +The primary driver APIs accept both the app and the codec used to configure it. +Tests should pass the same codec instance used in `WireframeApp::with_codec` to +ensure framing configuration (such as maximum frame length) matches. + +```rust,no_run +use std::io; +use wireframe::app::{Packet, WireframeApp}; +use wireframe::codec::FrameCodec; + +pub async fn drive_with_frames( + app: WireframeApp, + codec: &F, + frames: Vec, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, + F: FrameCodec; + +pub async fn drive_with_payloads( + app: WireframeApp, + codec: &F, + payloads: Vec>, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, + F: FrameCodec; + +pub async fn drive_with_raw_bytes( + app: WireframeApp, + wire_bytes: Vec>, + capacity: Option, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, + F: FrameCodec; ``` -The crate would live in a `wireframe_testing/` directory alongside the main -`wireframe` crate. +Behavioural details: -## Proposed API +- `drive_with_frames` encodes each `F::Frame` with `codec.encoder()`, writes the + resulting bytes to the duplex stream, reads the server response bytes, and + decodes them with `codec.decoder()`. +- `drive_with_payloads` wraps payload bytes using `F::wrap_payload` before + delegating to `drive_with_frames`. +- `drive_with_raw_bytes` writes the provided bytes verbatim without encoding or + decoding. This is the entry point for malformed frame tests and recovery + policy validation. +- Mutable variants (`drive_with_frames_mut` and `drive_with_payloads_mut`) + accept `&mut WireframeApp` so tests can reuse a configured instance. +- I/O failures, codec encode/decode failures, and server task panics are all + returned as `io::Error` values so tests can assert on error handling. -```rust -use tokio::io::Result as IoResult; -use wireframe::app::WireframeApp; -use serde::Serialize; +### Buffer capacity and limits + +The duplex stream buffer should default to the codec maximum frame length, +clamped to a shared safety ceiling that matches Wireframe's guardrail. The +driver should reject a `capacity` of zero or above this ceiling with +`io::ErrorKind::InvalidInput`. + +To keep the guardrail aligned, expose a public constant (or helper) from +`wireframe::codec` so `wireframe_testing` does not duplicate the limit. -/// Feed a single frame into `app` using an in-memory duplex stream. -pub async fn drive_with_frame(app: WireframeApp, frame: Vec) -> IoResult>; +### Frame encoding and decoding helpers -/// Drive `app` with multiple frames, returning all bytes written by the app. -pub async fn drive_with_frames(app: WireframeApp, frames: Vec>) -> IoResult>; +Codec-aware helpers make it easy to build fixtures or inspect raw bytes: -/// Encode `msg` with `bincode`, wrap it in a frame, and drive the app. -pub async fn drive_with_bincode(app: WireframeApp, msg: M) -> IoResult> +```rust,no_run +use std::io; +use wireframe::codec::FrameCodec; + +pub fn encode_frames(codec: &F, frames: Vec) -> io::Result> where - M: Serialize; -``` + F: FrameCodec; -These functions mirror the behaviour of the `run_app` helper found in the -repository’s test utilities. They create a `tokio::io::duplex` stream, run the -application on the server half, and write the provided frame(s) to the client -side. All helpers delegate to a single internal function that handles this I/O -plumbing, ensuring consistent behaviour. Should the application panic, the -panic message is returned as an `io::Error` beginning with -`server task failed`, helping surface failures in tests. After the application -finishes processing the input frames, the bytes written back are collected for -inspection. - -Any I/O errors surfaced by the duplex stream or failures while decoding a -length prefix propagate through the returned `IoResult`. Malformed or truncated -frames therefore cause the future to resolve with an error, allowing tests to -assert on these failure conditions directly. - -### Custom Buffer Capacity - -A variant accepting a buffer `capacity` allows fine-tuning the size of the -in-memory duplex channel, matching the existing `run_app` helper. The value -must be greater than zero and does not exceed 10 MB. - -```helpers. -pub async fn drive_with_frame_with_capacity( - app: WireframeApp, - frame: Vec, - capacity: usize, -) -> IoResult>; - -pub async fn drive_with_frames_with_capacity( - app: WireframeApp, - frames: Vec>, - capacity: usize, -) -> IoResult>; - -/// The above helpers consume the `WireframeApp`. For scenarios -/// where a single app instance should be reused across calls, -/// borrow it mutably instead. -pub async fn drive_with_frame_mut(app: &mut WireframeApp, frame: Vec) -> IoResult>; -pub async fn drive_with_frames_mut(app: &mut WireframeApp, frames: Vec>) -> IoResult>; +pub fn decode_frames(codec: &F, bytes: Vec) -> io::Result> +where + F: FrameCodec; ``` -### Bincode Convenience Wrapper +`decode_frames` should return an error when trailing bytes remain in the buffer +after the last frame, so tests can detect partial or malformed streams. -For most tests the input frame is preassembled from raw bytes. A small wrapper -can accept any `serde::Serialize` value and perform the encoding and framing -before delegating to `drive_with_frame`. The approach mirrors patterns in -`tests/routes.rs`, where structs convert to bytes with `BincodeSerializer` -before being wrapped in a length-prefixed frame. +### Bincode convenience wrapper -```rust -#[derive(serde::Serialize)] +Most tests still send a single request encoded with bincode. Keep a small +wrapper that performs `bincode::encode_to_vec` with +`bincode::config::standard()`, wraps the payload via `F::wrap_payload`, and +drives the app: + +```rust,no_run +#[derive(bincode::Encode)] struct Ping(u8); -let bytes = drive_with_bincode(app, Ping(1)).await.unwrap(); -assert_eq!(bytes, [0, 1]); +let frames = drive_with_bincode(app, &codec, Ping(1)).await?; +assert_eq!(F::frame_payload(&frames[0]), &[1]); ``` -### Helper macros +## Codec fixtures -Two small macros, `push_expect!` and `recv_expect!`, reduce boilerplate in test -code. They await a future and panic with a message including the call site when -the future resolves to an error. +Add reusable fixtures to avoid duplicated framing logic in tests: -```rust -push_expect!(handle.push_high_priority(42)); -let (_, frame) = recv_expect!(queues.recv()); -``` +- Default codec fixtures that yield `Bytes` frames, oversized payloads, and + truncated prefixes for negative tests. +- Example codec fixtures that mirror `wireframe::codec::examples` (for example + Hotline and MySQL), including helpers for correlation identifiers. +- Invalid frame builders (bad lengths, missing headers, truncated payloads) for + codec error and recovery assertions. -## Example Usage +Fixtures should return `F::Frame` where possible and provide explicit +`wire_bytes()` helpers for malformed cases. -```rust -use std::sync::Arc; -use wireframe_testing::{drive_with_frame, drive_with_frames}; -use crate::tests::{build_test_frame, expected_bytes}; +## Test observability harness -#[tokio::test] -async fn handler_echoes_message() { - let app = WireframeApp::new() - .unwrap() - .route(1, Arc::new(|_| Box::pin(async {}))) - .unwrap(); - - let frame = build_test_frame(); - let out = drive_with_frame(app, frame).await.unwrap(); - assert_eq!(out, expected_bytes()); +Introduce `wireframe_testing::observability`, providing an +`ObservabilityHandle` that combines log capture with metrics recording. + +Key behaviours: + +- Acquisition installs log capture via `LoggerHandle` and a scoped metrics + recorder using `metrics_util::debugging::DebuggingRecorder`. +- Access is serialized with a global lock so concurrent tests do not interfere. +- Metrics snapshots should consume the captured values (matching + `DebuggingRecorder` semantics) so `clear()` can be implemented by draining a + snapshot. +- The handle should restore the previous recorder on drop by swapping the + active recorder back into a global delegating recorder. This keeps the global + recorder stable while still providing per-test isolation. +- When the `metrics` feature is disabled, the handle should still capture logs + and return empty metric snapshots. + +Proposed public API: + +```rust,no_run +use metrics_util::debugging::Snapshot; +use wireframe_testing::LoggerHandle; + +pub struct ObservabilityHandle { /* fields omitted */ } + +impl ObservabilityHandle { + pub fn new() -> Self; + pub fn logs(&mut self) -> &mut LoggerHandle; + pub fn snapshot(&self) -> Snapshot; + pub fn clear(&mut self); + pub fn counter(&self, name: &str, labels: &[(&str, &str)]) -> u64; } + +pub fn observability() -> ObservabilityHandle; ``` -This pattern mirrors the style of `tests/routes.rs`, where handlers are invoked -with prebuilt frames and their responses decoded for assertions. +Tests using `ObservabilityHandle` should not run concurrently; the global lock +will serialize access, and the documentation must call this out explicitly. -## Benefits +## Helper macros -- **Isolation**: Handlers can be tested without spinning up a full server or - opening sockets. -- **Reusability**: Projects consuming `wireframe` can depend on - `wireframe_testing` in their dev-dependencies to leverage the same helpers. -- **Clarity**: Abstracting the duplex stream logic keeps test cases focused on - behaviour instead of transport details. +Keep `push_expect!` and `recv_expect!` for concise async assertions, with error +messages that include call-site information in debug builds. -### Capturing Logs in Tests +## Example usage -The `wireframe_testing` crate exposes a \[`LoggerHandle`\] fixture for -asserting log output. Acquire it in a test and call `clear()` to discard any -records from fixture setup. Records can then be inspected using `pop()`: +```rust,no_run +use wireframe::app::WireframeApp; +use wireframe::codec::LengthDelimitedFrameCodec; +use wireframe_testing::{drive_with_payloads, observability}; -```rust -use wireframe_testing::logger; +#[tokio::test] +async fn round_trips_with_codec() -> std::io::Result<()> { + let codec = LengthDelimitedFrameCodec::new(1024); + let app = WireframeApp::new()?.with_codec(codec.clone()); + let frames = drive_with_payloads(app, &codec, vec![b"ping".to_vec()]).await?; + assert_eq!( + LengthDelimitedFrameCodec::frame_payload(&frames[0]), + b"ping" + ); + Ok(()) +} #[tokio::test] -async fn captures_logs() { - let mut logs = logger(); - logs.clear(); - log::error!(target = "demo", key = 1, "boom"); - let record = logs.pop().unwrap(); - assert_eq!(record.target(), "demo"); +async fn captures_codec_metrics() -> std::io::Result<()> { + let mut obs = observability(); + obs.clear(); + + let codec = LengthDelimitedFrameCodec::new(16); + let app = WireframeApp::new()?.with_codec(codec.clone()); + let _ = wireframe_testing::drive_with_raw_bytes( + app, + vec![vec![0, 0, 0, 8, 1]], + None, + ) + .await?; + + assert_eq!(obs.counter("wireframe.codec.errors", &[]), 1); + Ok(()) } ``` -## Next Steps - -Implement the crate in a new directory, export the helper functions, and -migrate existing tests to use them. Additional fixtures (e.g., prebuilt frame -processors) can be added over time as test coverage grows. +## Implementation notes + +- Update all driver helpers to accept `F: FrameCodec` and operate on `F::Frame` + rather than raw length-delimited bytes. +- Add codec-aware `encode_frames` and `decode_frames` helpers that return + `io::Result` on failures instead of panicking. +- Provide a raw byte driver for malformed input and recovery tests. +- Add fixture helpers for default and example codecs, including invalid frames + used by codec error tests. +- Implement the observability harness and expose it via an `rstest` fixture in + `wireframe_testing::observability`. From f87176f23a089f0463de9681e2baa09ea9dcca93 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Sat, 3 Jan 2026 02:22:10 +0000 Subject: [PATCH 3/8] Address review feedback in docs Refine testing crate documentation for codec-aware drivers and observability guidance, and align the test observability ADR. Update the users guide framing note to remove second-person phrasing and clarify framed-stream usage. --- docs/adr-006-test-observability.md | 13 ++++++++----- docs/users-guide.md | 4 ++-- docs/wireframe-testing-crate.md | 23 ++++++++++++++--------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/docs/adr-006-test-observability.md b/docs/adr-006-test-observability.md index 956856d0..db9b6858 100644 --- a/docs/adr-006-test-observability.md +++ b/docs/adr-006-test-observability.md @@ -18,9 +18,9 @@ exporters, and there is no shared helper for asserting instrumentation. This makes telemetry assertions inconsistent, brittle, and hard to reuse across downstream crates. -A dedicated test observability harness is needed so tests can assert on logging -and metrics deterministically without depending on external exporters or global -state leaks. +A dedicated test observability harness is needed, so tests can assert on +logging and metrics deterministically without depending on external exporters +or global state leaks. ## Decision Drivers @@ -76,14 +76,17 @@ and behavioural tests. exporters. - Provide helper methods to clear state between assertions and to filter by labels when verifying counters. -- Serialise access with a global lock to avoid cross-test interference, and +- Serialize access with a global lock to avoid cross-test interference, and document that tests using the handle should not run concurrently. +- For parallel test runners, run observability-heavy test binaries with + `--test-threads=1` or gate observability tests behind a shared fixture to + prevent interleaving. ## Consequences - Tests can assert on instrumentation for codec errors and recovery policies without bespoke setup. -- Global recorder access requires serialisation, which may reduce parallelism +- Global recorder access requires serialization, which may reduce parallelism for observability-heavy test suites. - The testing crate gains additional dependency surface for metrics capture. diff --git a/docs/users-guide.md b/docs/users-guide.md index 9e77df6e..e4337526 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -139,8 +139,8 @@ A codec implementation must: Install a custom codec with `with_codec`. The builder resets fragmentation to the codec-derived defaults, so override fragmentation afterwards if the -protocol uses a different budget. When you already have a framed stream, use -`send_response_framed_with_codec` so responses pass through +protocol uses a different budget. When a framed stream is already available, +use `send_response_framed_with_codec`, so responses pass through `FrameCodec::wrap_payload`. Assume `MyCodec` implements `FrameCodec`: diff --git a/docs/wireframe-testing-crate.md b/docs/wireframe-testing-crate.md index 4146cc66..b0319505 100644 --- a/docs/wireframe-testing-crate.md +++ b/docs/wireframe-testing-crate.md @@ -11,8 +11,8 @@ Wireframe now supports pluggable protocol codecs (ADR 004). The test harness must encode and decode frames using the selected `FrameCodec`, preserving protocol metadata and correlation identifiers. It must also allow malformed wire bytes for negative codec tests. ADR 006 proposes a unified test -observability harness so tests can assert on logs and metrics deterministically -without external exporters. +observability harness, so tests can assert on logs and metrics +deterministically without external exporters. ## Design goals @@ -75,9 +75,11 @@ pub trait TestSerializer: ### Driver entry points -The primary driver APIs accept both the app and the codec used to configure it. +Codec-aware driver APIs accept both the app and the codec used to configure it. Tests should pass the same codec instance used in `WireframeApp::with_codec` to -ensure framing configuration (such as maximum frame length) matches. +ensure framing configuration (such as maximum frame length) matches. The +raw-byte driver only needs the app because it writes bytes verbatim and returns +the raw output. ```rust,no_run use std::io; @@ -114,8 +116,7 @@ pub async fn drive_with_raw_bytes( where S: TestSerializer, C: Send + 'static, - E: Packet, - F: FrameCodec; + E: Packet; ``` Behavioural details: @@ -131,7 +132,7 @@ Behavioural details: - Mutable variants (`drive_with_frames_mut` and `drive_with_payloads_mut`) accept `&mut WireframeApp` so tests can reuse a configured instance. - I/O failures, codec encode/decode failures, and server task panics are all - returned as `io::Error` values so tests can assert on error handling. + returned as `io::Error` values, so tests can assert on error handling. ### Buffer capacity and limits @@ -201,7 +202,11 @@ Key behaviours: - Acquisition installs log capture via `LoggerHandle` and a scoped metrics recorder using `metrics_util::debugging::DebuggingRecorder`. -- Access is serialized with a global lock so concurrent tests do not interfere. +- Access is serialized with a global lock, so concurrent tests do not + interfere. +- Observability-heavy suites should run in a single-threaded test runner (for + example, pass `--test-threads=1` for the affected test binary or gate the + tests behind a shared fixture), so metrics and logs do not interleave. - Metrics snapshots should consume the captured values (matching `DebuggingRecorder` semantics) so `clear()` can be implemented by draining a snapshot. @@ -231,7 +236,7 @@ pub fn observability() -> ObservabilityHandle; ``` Tests using `ObservabilityHandle` should not run concurrently; the global lock -will serialize access, and the documentation must call this out explicitly. +serializes access, and the documentation must call this out explicitly. ## Helper macros From 9e3cc4a9e43632ed6b89a3b6187a9fe0f70b7b9b Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Sat, 3 Jan 2026 03:17:42 +0000 Subject: [PATCH 4/8] Fix British spelling in codec guide Update the custom codec guidance to use en-GB spelling for "deserialisation". --- docs/users-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/users-guide.md b/docs/users-guide.md index e4337526..c89f64d7 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -128,7 +128,7 @@ A codec implementation must: - Define a `Frame` type and paired decoder/encoder implementations that return `std::io::Error` on failure. - Return only the logical payload bytes from `frame_payload` so metadata parsing - and deserialization run against the right buffer. + and deserialisation run against the right buffer. - Wrap outbound payloads with `wrap_payload`, adding any protocol headers or metadata required by the wire format. - Provide `correlation_id` when the protocol stores it outside the payload; From 8b9f1a227c9e2e63bf4e79e1b0b9efeeb5f4e4db Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Sat, 3 Jan 2026 13:51:54 +0000 Subject: [PATCH 5/8] Refine test observability docs Expand the testing crate design with mutable driver signatures and a metrics example that aligns with existing counters. Clarify the test observability ADR consequences with concrete parallelism guidance. --- docs/adr-006-test-observability.md | 3 ++- docs/wireframe-testing-crate.md | 41 ++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/docs/adr-006-test-observability.md b/docs/adr-006-test-observability.md index db9b6858..fc7b55a6 100644 --- a/docs/adr-006-test-observability.md +++ b/docs/adr-006-test-observability.md @@ -87,7 +87,8 @@ and behavioural tests. - Tests can assert on instrumentation for codec errors and recovery policies without bespoke setup. - Global recorder access requires serialization, which may reduce parallelism - for observability-heavy test suites. + for observability-heavy suites, such as forcing `--test-threads=1` for the + affected test binary. See the mitigation guidance in the approach. - The testing crate gains additional dependency surface for metrics capture. ## Roadmap diff --git a/docs/wireframe-testing-crate.md b/docs/wireframe-testing-crate.md index b0319505..0c618f56 100644 --- a/docs/wireframe-testing-crate.md +++ b/docs/wireframe-testing-crate.md @@ -117,6 +117,28 @@ where S: TestSerializer, C: Send + 'static, E: Packet; + +pub async fn drive_with_frames_mut( + app: &mut WireframeApp, + codec: &F, + frames: Vec, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, + F: FrameCodec; + +pub async fn drive_with_payloads_mut( + app: &mut WireframeApp, + codec: &F, + payloads: Vec>, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, + F: FrameCodec; ``` Behavioural details: @@ -263,20 +285,17 @@ async fn round_trips_with_codec() -> std::io::Result<()> { } #[tokio::test] -async fn captures_codec_metrics() -> std::io::Result<()> { +async fn captures_metrics() -> std::io::Result<()> { + use wireframe::metrics::{Direction, FRAMES_PROCESSED, inc_frames}; + let mut obs = observability(); obs.clear(); - let codec = LengthDelimitedFrameCodec::new(16); - let app = WireframeApp::new()?.with_codec(codec.clone()); - let _ = wireframe_testing::drive_with_raw_bytes( - app, - vec![vec![0, 0, 0, 8, 1]], - None, - ) - .await?; - - assert_eq!(obs.counter("wireframe.codec.errors", &[]), 1); + inc_frames(Direction::Inbound); + assert_eq!( + obs.counter(FRAMES_PROCESSED, &[("direction", "inbound")]), + 1 + ); Ok(()) } ``` From be37037fd9978f56a7ff6971705de52c978a62a3 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Sat, 3 Jan 2026 14:19:26 +0000 Subject: [PATCH 6/8] Add payload driver helpers Introduce payload-driven helpers in wireframe_testing and re-export them for tests. Update the testing crate design doc to reflect the current helper signatures, correct the bincode wrapper example, and expand the observability concurrency guidance. --- docs/wireframe-testing-crate.md | 162 ++++++++++++++++--------------- wireframe_testing/src/helpers.rs | 131 ++++++++++++++++++++++++- wireframe_testing/src/lib.rs | 2 + 3 files changed, 216 insertions(+), 79 deletions(-) diff --git a/docs/wireframe-testing-crate.md b/docs/wireframe-testing-crate.md index 0c618f56..c1e7e13d 100644 --- a/docs/wireframe-testing-crate.md +++ b/docs/wireframe-testing-crate.md @@ -75,96 +75,79 @@ pub trait TestSerializer: ### Driver entry points -Codec-aware driver APIs accept both the app and the codec used to configure it. -Tests should pass the same codec instance used in `WireframeApp::with_codec` to -ensure framing configuration (such as maximum frame length) matches. The -raw-byte driver only needs the app because it writes bytes verbatim and returns -the raw output. +Length-delimited helpers match the current `wireframe_testing` API and accept +raw frame bytes. Use `drive_with_frames` for pre-framed input (including +malformed frames) and `drive_with_payloads` to wrap payloads with the default +length-delimited framing. ```rust,no_run use std::io; use wireframe::app::{Packet, WireframeApp}; -use wireframe::codec::FrameCodec; -pub async fn drive_with_frames( - app: WireframeApp, - codec: &F, - frames: Vec, -) -> io::Result> +pub async fn drive_with_frames( + app: WireframeApp, + frames: Vec>, +) -> io::Result> where S: TestSerializer, C: Send + 'static, - E: Packet, - F: FrameCodec; + E: Packet; -pub async fn drive_with_payloads( - app: WireframeApp, - codec: &F, +pub async fn drive_with_payloads( + app: WireframeApp, payloads: Vec>, -) -> io::Result> -where - S: TestSerializer, - C: Send + 'static, - E: Packet, - F: FrameCodec; - -pub async fn drive_with_raw_bytes( - app: WireframeApp, - wire_bytes: Vec>, - capacity: Option, ) -> io::Result> where S: TestSerializer, C: Send + 'static, E: Packet; -pub async fn drive_with_frames_mut( - app: &mut WireframeApp, - codec: &F, - frames: Vec, -) -> io::Result> +pub async fn drive_with_frames_mut( + app: &mut WireframeApp, + frames: Vec>, +) -> io::Result> where S: TestSerializer, C: Send + 'static, - E: Packet, - F: FrameCodec; + E: Packet; -pub async fn drive_with_payloads_mut( - app: &mut WireframeApp, - codec: &F, +pub async fn drive_with_payloads_mut( + app: &mut WireframeApp, payloads: Vec>, -) -> io::Result> +) -> io::Result> where S: TestSerializer, C: Send + 'static, - E: Packet, - F: FrameCodec; + E: Packet; ``` +Codec-aware helpers should be added as non-breaking extensions so tests can +pass `FrameCodec` values and inspect protocol-specific frame metadata. Prefer +distinct names (for example, `drive_with_codec_frames`) so existing tests that +use raw byte frames continue to compile unchanged. + Behavioural details: -- `drive_with_frames` encodes each `F::Frame` with `codec.encoder()`, writes the - resulting bytes to the duplex stream, reads the server response bytes, and - decodes them with `codec.decoder()`. -- `drive_with_payloads` wraps payload bytes using `F::wrap_payload` before - delegating to `drive_with_frames`. -- `drive_with_raw_bytes` writes the provided bytes verbatim without encoding or - decoding. This is the entry point for malformed frame tests and recovery - policy validation. +- `drive_with_frames` writes the provided bytes verbatim, making it the + preferred helper for malformed frame and recovery tests. +- `drive_with_payloads` length-prefixes payload bytes before delegating to + `drive_with_frames`. +- `drive_with_bincode` encodes a message with bincode and then length-prefixes + the output before driving the app. - Mutable variants (`drive_with_frames_mut` and `drive_with_payloads_mut`) accept `&mut WireframeApp` so tests can reuse a configured instance. -- I/O failures, codec encode/decode failures, and server task panics are all - returned as `io::Error` values, so tests can assert on error handling. +- I/O failures, framing errors, and server task panics are all returned as + `io::Error` values, so tests can assert on error handling. ### Buffer capacity and limits -The duplex stream buffer should default to the codec maximum frame length, -clamped to a shared safety ceiling that matches Wireframe's guardrail. The -driver should reject a `capacity` of zero or above this ceiling with -`io::ErrorKind::InvalidInput`. +The duplex stream buffer defaults to `TEST_MAX_FRAME`, matching the shared +length-delimited framing guardrail. Use `run_app` or the `*_with_capacity` +helpers to override this value; they reject a `capacity` of zero or above the +maximum ceiling with `io::ErrorKind::InvalidInput`. -To keep the guardrail aligned, expose a public constant (or helper) from -`wireframe::codec` so `wireframe_testing` does not duplicate the limit. +Codec-aware helpers should instead read `FrameCodec::max_frame_length()` to +align buffer sizing with protocol framing rules. ### Frame encoding and decoding helpers @@ -190,15 +173,32 @@ after the last frame, so tests can detect partial or malformed streams. Most tests still send a single request encoded with bincode. Keep a small wrapper that performs `bincode::encode_to_vec` with -`bincode::config::standard()`, wraps the payload via `F::wrap_payload`, and -drives the app: +`bincode::config::standard()` and drives the app: ```rust,no_run +use std::io; +use wireframe::app::{Packet, WireframeApp}; + +pub async fn drive_with_bincode( + app: WireframeApp, + msg: M, +) -> io::Result> +where + M: bincode::Encode, + S: TestSerializer, + C: Send + 'static, + E: Packet; +``` + +```rust,no_run +use wireframe_testing::{decode_frames, drive_with_bincode}; + #[derive(bincode::Encode)] struct Ping(u8); -let frames = drive_with_bincode(app, &codec, Ping(1)).await?; -assert_eq!(F::frame_payload(&frames[0]), &[1]); +let bytes = drive_with_bincode(app, Ping(1)).await?; +let frames = decode_frames(bytes); +assert_eq!(frames[0], vec![1]); ``` ## Codec fixtures @@ -225,10 +225,13 @@ Key behaviours: - Acquisition installs log capture via `LoggerHandle` and a scoped metrics recorder using `metrics_util::debugging::DebuggingRecorder`. - Access is serialized with a global lock, so concurrent tests do not - interfere. + interfere, but the harness will reduce parallelism for the affected suite. - Observability-heavy suites should run in a single-threaded test runner (for - example, pass `--test-threads=1` for the affected test binary or gate the - tests behind a shared fixture), so metrics and logs do not interleave. + example, pass `--test-threads=1` for the affected test binary), or share a + single `ObservabilityHandle` via a per-suite fixture to amortise setup costs. +- When partial parallelism is needed, group observability assertions into a + dedicated test binary that runs serially, and keep the remaining test suite + in the default parallel runner. - Metrics snapshots should consume the captured values (matching `DebuggingRecorder` semantics) so `clear()` can be implemented by draining a snapshot. @@ -258,7 +261,8 @@ pub fn observability() -> ObservabilityHandle; ``` Tests using `ObservabilityHandle` should not run concurrently; the global lock -serializes access, and the documentation must call this out explicitly. +serializes access, so favour a shared fixture or a dedicated serial test binary +for observability assertions. ## Helper macros @@ -268,19 +272,19 @@ messages that include call-site information in debug builds. ## Example usage ```rust,no_run -use wireframe::app::WireframeApp; -use wireframe::codec::LengthDelimitedFrameCodec; -use wireframe_testing::{drive_with_payloads, observability}; +use std::sync::Arc; + +use wireframe::app::{Envelope, WireframeApp}; +use wireframe_testing::{decode_frames, drive_with_bincode, observability}; #[tokio::test] async fn round_trips_with_codec() -> std::io::Result<()> { - let codec = LengthDelimitedFrameCodec::new(1024); - let app = WireframeApp::new()?.with_codec(codec.clone()); - let frames = drive_with_payloads(app, &codec, vec![b"ping".to_vec()]).await?; - assert_eq!( - LengthDelimitedFrameCodec::frame_payload(&frames[0]), - b"ping" - ); + let app = WireframeApp::new()? + .route(1, Arc::new(|_: &Envelope| Box::pin(async {})))?; + let env = Envelope::new(1, Some(5), vec![1, 2, 3]); + let out = drive_with_bincode(app, env).await?; + let frames = decode_frames(out); + assert_eq!(frames.len(), 1); Ok(()) } @@ -302,11 +306,13 @@ async fn captures_metrics() -> std::io::Result<()> { ## Implementation notes -- Update all driver helpers to accept `F: FrameCodec` and operate on `F::Frame` - rather than raw length-delimited bytes. -- Add codec-aware `encode_frames` and `decode_frames` helpers that return +- Add codec-aware helpers (for example, `drive_with_codec_frames`) that accept + `F: FrameCodec` and return `F::Frame` values for tests that need protocol + metadata. +- Provide codec-aware `encode_frames` and `decode_frames` helpers that return `io::Result` on failures instead of panicking. -- Provide a raw byte driver for malformed input and recovery tests. +- Keep the length-delimited helpers so existing tests that use raw frame bytes + remain compatible. - Add fixture helpers for default and example codecs, including invalid frames used by codec error tests. - Implement the observability harness and expose it via an `rstest` fixture in diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index d74b90f2..3c2d73fa 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -279,6 +279,107 @@ where .await } +/// Encode payloads as length-delimited frames and drive `app`. +/// +/// This helper wraps each payload using the default length-delimited framing +/// format before sending it to the application. +/// +/// ```rust +/// # use wireframe_testing::drive_with_payloads; +/// # use wireframe::app::WireframeApp; +/// # async fn demo() -> std::io::Result<()> { +/// let app = WireframeApp::new().expect("failed to initialize app"); +/// let out = drive_with_payloads(app, vec![vec![1], vec![2]]).await?; +/// # let _ = out; +/// # Ok(()) +/// # } +/// ``` +pub async fn drive_with_payloads( + app: WireframeApp, + payloads: Vec>, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + drive_with_payloads_with_capacity(app, payloads, DEFAULT_CAPACITY).await +} + +/// Encode payloads as length-delimited frames and drive a mutable `app`. +/// +/// ```rust +/// # use wireframe_testing::drive_with_payloads_mut; +/// # use wireframe::app::WireframeApp; +/// # async fn demo() -> std::io::Result<()> { +/// let mut app = WireframeApp::new().expect("failed to initialize app"); +/// let out = drive_with_payloads_mut(&mut app, vec![vec![1], vec![2]]).await?; +/// # let _ = out; +/// # Ok(()) +/// # } +/// ``` +pub async fn drive_with_payloads_mut( + app: &mut WireframeApp, + payloads: Vec>, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + drive_with_payloads_with_capacity_mut(app, payloads, DEFAULT_CAPACITY).await +} + +fn encode_payloads( + payloads: Vec>, + mut codec: LengthDelimitedCodec, +) -> io::Result>> { + payloads + .into_iter() + .map(|payload| { + let header_len = LengthFormat::default().bytes(); + let mut buf = BytesMut::with_capacity(payload.len() + header_len); + codec.encode(payload.into(), &mut buf).map_err(|err| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("frame encode failed: {err}"), + ) + })?; + Ok(buf.to_vec()) + }) + .collect() +} + +async fn drive_with_payloads_with_capacity( + app: WireframeApp, + payloads: Vec>, + capacity: usize, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + let codec = new_test_codec(DEFAULT_CAPACITY); + let frames = encode_payloads(payloads, codec)?; + drive_with_frames_with_capacity(app, frames, capacity).await +} + +async fn drive_with_payloads_with_capacity_mut( + app: &mut WireframeApp, + payloads: Vec>, + capacity: usize, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + let codec = new_test_codec(DEFAULT_CAPACITY); + let frames = encode_payloads(payloads, codec)?; + drive_with_frames_with_capacity_mut(app, frames, capacity).await +} + forward_default! { /// Feed a single frame into a mutable `app`, allowing the instance to be reused /// across calls. @@ -472,7 +573,13 @@ where #[cfg(test)] mod tests { - use wireframe::app::WireframeApp; + use std::sync::Arc; + + use wireframe::{ + Serializer, + app::{Envelope, WireframeApp}, + serializer::BincodeSerializer, + }; use super::*; @@ -493,6 +600,28 @@ mod tests { .expect_err("capacity beyond max should error"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); } + + #[tokio::test] + async fn drive_with_payloads_wraps_frames() -> io::Result<()> { + let app = WireframeApp::new()?.route(1, Arc::new(|_: &Envelope| Box::pin(async {})))?; + let serializer = BincodeSerializer::default(); + let payload = vec![1_u8, 2, 3]; + let env = Envelope::new(1, Some(7), payload.clone()); + let encoded = serializer + .serialize(&env) + .expect("failed to serialize envelope"); + + let out = drive_with_payloads(app, vec![encoded]).await?; + let frames = decode_frames(out); + let [first] = frames.as_slice() else { + panic!("expected a single response frame"); + }; + let (decoded, _) = serializer + .deserialize::(first) + .expect("failed to deserialise envelope"); + assert_eq!(decoded.payload, payload, "payload mismatch"); + Ok(()) + } } /// Run `app` against an empty duplex stream. diff --git a/wireframe_testing/src/lib.rs b/wireframe_testing/src/lib.rs index 4244d5bc..7ac614ee 100644 --- a/wireframe_testing/src/lib.rs +++ b/wireframe_testing/src/lib.rs @@ -34,6 +34,8 @@ pub use helpers::{ drive_with_frames, drive_with_frames_mut, drive_with_frames_with_capacity, + drive_with_payloads, + drive_with_payloads_mut, encode_frame, new_test_codec, run_app, From 610c46ac7ccb23480e1544d51455fd0b80807be3 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Sat, 3 Jan 2026 19:44:43 +0000 Subject: [PATCH 7/8] Clarify testing crate docs Adjust punctuation and clarify which codec helpers are proposed, plus correct the bincode example assertions and spelling. --- docs/wireframe-testing-crate.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/wireframe-testing-crate.md b/docs/wireframe-testing-crate.md index c1e7e13d..03bf42a3 100644 --- a/docs/wireframe-testing-crate.md +++ b/docs/wireframe-testing-crate.md @@ -121,7 +121,7 @@ where E: Packet; ``` -Codec-aware helpers should be added as non-breaking extensions so tests can +Codec-aware helpers should be added as non-breaking extensions, so tests can pass `FrameCodec` values and inspect protocol-specific frame metadata. Prefer distinct names (for example, `drive_with_codec_frames`) so existing tests that use raw byte frames continue to compile unchanged. @@ -151,7 +151,8 @@ align buffer sizing with protocol framing rules. ### Frame encoding and decoding helpers -Codec-aware helpers make it easy to build fixtures or inspect raw bytes: +Proposed codec-aware helpers make it easy to build fixtures or inspect raw +bytes: ```rust,no_run use std::io; @@ -198,7 +199,7 @@ struct Ping(u8); let bytes = drive_with_bincode(app, Ping(1)).await?; let frames = decode_frames(bytes); -assert_eq!(frames[0], vec![1]); +assert!(!frames.is_empty(), "expected at least one response frame"); ``` ## Codec fixtures @@ -228,7 +229,7 @@ Key behaviours: interfere, but the harness will reduce parallelism for the affected suite. - Observability-heavy suites should run in a single-threaded test runner (for example, pass `--test-threads=1` for the affected test binary), or share a - single `ObservabilityHandle` via a per-suite fixture to amortise setup costs. + single `ObservabilityHandle` via a per-suite fixture to amortize setup costs. - When partial parallelism is needed, group observability assertions into a dedicated test binary that runs serially, and keep the remaining test suite in the default parallel runner. From 78a42714e375e94821e07d95bd1431d7641e341f Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Sun, 4 Jan 2026 01:37:16 +0000 Subject: [PATCH 8/8] Separate current and proposed helpers Clarify the testing crate documentation by splitting current frame helper signatures from proposed codec-aware additions. Update the bincode example assertion and punctuation to avoid confusion about available APIs. --- docs/wireframe-testing-crate.md | 43 +++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/docs/wireframe-testing-crate.md b/docs/wireframe-testing-crate.md index 03bf42a3..6a28b56d 100644 --- a/docs/wireframe-testing-crate.md +++ b/docs/wireframe-testing-crate.md @@ -151,24 +151,18 @@ align buffer sizing with protocol framing rules. ### Frame encoding and decoding helpers -Proposed codec-aware helpers make it easy to build fixtures or inspect raw -bytes: +The current helpers focus on the default length-delimited framing used by +Wireframe tests: ```rust,no_run -use std::io; -use wireframe::codec::FrameCodec; +use tokio_util::codec::LengthDelimitedCodec; -pub fn encode_frames(codec: &F, frames: Vec) -> io::Result> -where - F: FrameCodec; +pub fn decode_frames(bytes: Vec) -> Vec>; -pub fn decode_frames(codec: &F, bytes: Vec) -> io::Result> -where - F: FrameCodec; -``` +pub fn decode_frames_with_max(bytes: Vec, max_len: usize) -> Vec>; -`decode_frames` should return an error when trailing bytes remain in the buffer -after the last frame, so tests can detect partial or malformed streams. +pub fn encode_frame(codec: &mut LengthDelimitedCodec, bytes: Vec) -> Vec; +``` ### Bincode convenience wrapper @@ -318,3 +312,26 @@ async fn captures_metrics() -> std::io::Result<()> { used by codec error tests. - Implement the observability harness and expose it via an `rstest` fixture in `wireframe_testing::observability`. + +## Proposed enhancements + +### Codec-aware frame encoding and decoding + +Proposed codec-aware helpers make it easy to build fixtures or inspect raw +bytes: + +```rust,no_run +use std::io; +use wireframe::codec::FrameCodec; + +pub fn encode_frames(codec: &F, frames: Vec) -> io::Result> +where + F: FrameCodec; + +pub fn decode_frames(codec: &F, bytes: Vec) -> io::Result> +where + F: FrameCodec; +``` + +These helpers should return an error when trailing bytes remain in the buffer +after the last frame, so tests can detect partial or malformed streams.