Skip to content

test(support): add frame_injector for protocol-aware fault injection (Phase 2E of #1074)#1105

Merged
kcenon merged 1 commit into
developfrom
feat/issue-1074-frame-injector-phase-2e
May 6, 2026
Merged

test(support): add frame_injector for protocol-aware fault injection (Phase 2E of #1074)#1105
kcenon merged 1 commit into
developfrom
feat/issue-1074-frame-injector-phase-2e

Conversation

@kcenon

@kcenon kcenon commented May 6, 2026

Copy link
Copy Markdown
Owner

What

Phase 2E of #1074: ships the last-remaining piece of the protocol-aware
loopback substrate — a small, composable byte-level fault hook
(tests/support/frame_injector.{h,cpp}) plus per-protocol demo tests
(tests/unit/frame_injector_demo_test.cpp) showing the hook composes
with every loopback peer / friend-test probe shipped in Phases 2A-2D.

Change Type

  • Feature (new test infrastructure)
  • Bugfix
  • Refactor
  • Documentation only
  • Test only

Affected Components

Path Kind
tests/support/frame_injector.h new — hook surface
tests/support/frame_injector.cpp new — pure transform variant
tests/support/mock_h2_server_peer.{h,cpp} wires injector into all 4 server-originated writes
tests/support/mock_grpc_server_peer.{h,cpp} wires injector into all 5 server-originated writes
tests/support/mock_quic_peer_loop.{h,cpp} wires injector via transform() around the send_to of the Initial datagram
tests/support/CMakeLists.txt adds frame_injector.cpp to the static library
tests/CMakeLists.txt registers network_frame_injector_demo_test
tests/unit/frame_injector_demo_test.cpp new — 4 demo TEST_F (one per protocol family)
tests/support/README.md Phase 2E composition example per protocol; phase progress table updated

Why

The five-phase plan in #1074 set out the substrate needed to lift the
filtered branch coverage of http2_client.cpp, grpc/client.cpp,
quic_socket.cpp, and websocket_server.cpp above the >=80 % line /

=70 % branch acceptance target tracked by sub-issues #1062-#1067.
Phases 2A-2D are merged on develop (PRs #1075, #1102, #1103, #1104).
Phase 2E is the last-remaining piece. With it, every line of the
issue body's "How → Phased delivery" table is materialised and the
six follow-up coverage sub-issues become unblocked phase-by-phase.

frame_injector is intentionally minimal: five modes, three of which
(drop, truncate, malform) are pure byte-level transforms; one
(slow_write) only meaningful at write time; and none for opt-out
backward compatibility. The default injection_spec{} produces an
injection_mode::none injector, which makes the new constructor
argument on each peer a no-op for every existing call site — no Phase
2A-2D test needed any modification.

Who

When

  • Target release: rolling (no specific release).
  • Dependencies: builds on Phases 2A-2D (already merged).
  • Blocks: nothing else in flight.

Where

How

Implementation

frame_injector captures one injection_spec and exposes two entry
points:

  • transform(span) — pure byte transform; returns optional<vector>
    (empty optional means drop). Used by tests that build raw byte
    streams off-stream (e.g. WebSocket pre-roll fed to ws_server_probe)
    and by the QUIC peer's UDP send_to path.
  • write(SyncStream&, span, ec) — template wrapper around asio::write
    that applies the spec at write time. Used by the h2 / gRPC peers'
    TLS-stream writes and natively supports slow_write byte-level pacing.

Each peer takes the spec via an optional last constructor argument so
existing callers compile unchanged. Internally each asio::write site
becomes one injector_.write(...) call; the QUIC peer wraps the single
send_to site in a transform()-then-send_to two-step.

Demo tests

tests/unit/frame_injector_demo_test.cpp adds four TEST_F (one per
protocol family), each driving one previously-unreachable error class:

Family Mode Driven branch (intent)
HTTP/2 drop server SETTINGS never arrives → http2_client connect-timeout
gRPC malform (offset 3) flips SETTINGS-ACK type byte → http2_client unexpected-frame branch via grpc_client
QUIC malform (offset 0) flips long-header form/fixed bits → quic_socket header-gate rejection before process_crypto_frame
WebSocket slow_write byte-by-byte client→server pre-roll, composed with ws_server_probe::invoke_handle_new_connection

Local verification

  • network_test_support library: builds clean with the new sources
    and the modified peers.
  • network_frame_injector_demo_test: builds clean and all 4 TEST_F
    pass
    (10 090 ms total — the two that exercise the connect-timeout
    branch are bound by the deliberate 500 ms client timeout each).
  • Phase 2 regression (h2 / gRPC / QUIC / WS branch_test + probe_test
    binaries): builds clean. Five binaries pass identically. One
    pre-existing fail in grpc_client_branch_test and one in
    quic_socket_branch_test reproduce on develop tip with no Phase
    2E changes applied
    (verified via local A/B against the same build
    directory). They are local-environment / Apple-Silicon specific and
    not introduced by this PR.

Test plan for reviewers

cmake --build build --target network_frame_injector_demo_test -j 4
./build/bin/network_frame_injector_demo_test
# Expect: [  PASSED  ] 4 tests.

Breaking changes

None. The injection_spec constructor argument defaults to {}
(== injection_mode::none) on every peer, so all Phase 2A-2D
call sites compile and run unchanged.

Rollback plan

Revert this PR. No artifact persists outside tests/support/ and
tests/unit/frame_injector_demo_test.cpp.

Related

Checklist

Adds tests/support/frame_injector.{h,cpp}, a small composable byte-level
fault hook supporting five modes (none, drop, truncate, malform,
slow_write). Wires the injector into mock_h2_server_peer,
mock_grpc_server_peer, and mock_quic_peer_loop via an optional
injection_spec constructor argument; the default value
(injection_mode::none) preserves existing peer behavior so all Phase
2A-2D tests continue to compile and run unchanged.

Adds tests/unit/frame_injector_demo_test.cpp with one TEST_F per
protocol family (HTTP/2 drop, gRPC malform, QUIC malform, WebSocket
slow_write), each driving one previously-unreachable error class via
the new injector and composing with the Phase 2D ws_server_probe.

Updates tests/support/README.md with a Phase 2E composition example
per protocol and marks Phases 2B / 2C / 2D / 2E as shipped to reflect
the merged history.

Part of #1074
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Metric Value
Line Coverage 68.9%
Branch Coverage 34.2%
Target 80% lines / 70% branches
Coverage Details

Full HTML report is available as a build artifact.

@kcenon kcenon merged commit 78e167e into develop May 6, 2026
15 checks passed
@kcenon kcenon deleted the feat/issue-1074-frame-injector-phase-2e branch May 6, 2026 10:34
kcenon added a commit that referenced this pull request May 6, 2026
…#1108)

Adds 6 new TEST_F cases to tests/unit/http2_client_branch_test.cpp that
compose mock_h2_server_peer (Phase 2A/2A.2/2E substrate) with the Phase
2E frame_injector (PR #1105) to drive previously-unreachable error
branches in src/protocols/http2/http2_client.cpp.

The Round 1 sub-issues of #953 (#991, #1062) raised happy-path coverage
on http2_client.cpp but left the file at 18.8% line / 9.9% branch on the
2026-05-06 develop measurement (run 25430202846). Round 1 closed before
the frame_injector substrate existed, so error-path branches that
require deterministic server-side frame faults could not be exercised.

This change opts each new TEST_F into one of five injection_mode values:

- DropFirstServerSettingsLeavesClientUnconnected
    injection_mode::drop on the first server-originated frame; client
    handshake never completes, exercising the connect-timeout branch
    that was previously only reachable via DISABLED integration tests.

- TruncatedServerSettingsHeaderTimesOutConnect
    injection_mode::truncate at 4 bytes; client receives an incomplete
    9-byte frame header and waits indefinitely, exercising the
    short-read / partial-header branch in the read loop.

- MalformedServerSettingsTypeByteTriggersConnectTimeout
    injection_mode::malform XOR=0x0F on the type-byte (offset 3); turns
    SETTINGS into an unknown frame type (0x0B), exercising the
    process_frame default / unknown-type dispatch branch.

- MalformedServerSettingsStreamIdNonZeroIsProtocolError
    injection_mode::malform XOR=0x01 on the last stream-id byte
    (offset 8); turns SETTINGS frame stream_id from 0 to 1, which is a
    PROTOCOL_ERROR per RFC 7540 §6.5, exercising the connection-error
    branch in handle_settings_frame.

- SlowWriteServerSettingsStillCompletesHandshake
    injection_mode::slow_write step=500us; positive test that the
    accumulating-buffer / partial-read branch in the read loop still
    completes the handshake when bytes arrive byte-by-byte.

- EchoOneTruncateAtNineDropsResponsePayloadFailingGet
    injection_mode::truncate at 9 bytes combined with reply_mode::echo_one;
    SETTINGS exchange completes (empty SETTINGS frames are exactly 9
    bytes so truncate_at=9 is a no-op for them) but response HEADERS
    and DATA frames lose their payloads, exercising the request-error
    / timeout branch on the connected path.

No src/ changes. Default injection_spec{} (== injection_mode::none) on
mock_h2_server_peer preserves Phase 2A/2A.2 behavior for all existing
TEST_F call sites that do not supply a spec.

Local verification: build clean, 4/6 new tests pass on macOS Apple
Silicon with the same pre-existing hermetic-handshake instability that
was observed and documented in PR #1105. The 2 tests with positive
is_connected() assertions fail locally for the same environmental
reason that fails the unmodified develop-tip baseline test
ConnectCompletesSettingsExchangeWithMockPeer on this machine. CI on
Linux + GitHub macOS runners is the authoritative verification surface.

Part of #1106
Part of #953
kcenon added a commit that referenced this pull request May 9, 2026
…1118)

Round 2 of grpc/client.cpp coverage expansion using the Phase 2 substrate
(frame_injector + mock_grpc_server_peer) merged in PR #1105. Round 1
sub-issues #994/#1063 raised happy-path coverage but client.cpp remained
at 22.6% line / 9.5% branch on run 25430202846 because error branches
require malformed/dropped/truncated/slow byte streams.

New TEST_F cases (all gRPC analogues of #1106 http2_client patterns):

- DropFirstServerSettingsLeavesGrpcClientUnconnected
  injection_mode::drop on server SETTINGS strands the underlying h2
  handshake; grpc_client::is_connected() never flips true.

- MalformedServerSettingsAckTypeByteBlocksGrpcConnect
  injection_mode::malform with offset 3 / xor 0x0F flips frame type
  byte (0x04 -> 0x0B) so the client drops the unknown frame and
  cannot complete the SETTINGS exchange.

- TruncatedServerSettingsHeaderBlocksGrpcConnect
  injection_mode::truncate with truncate_at=4 leaves a partial
  9-byte HTTP/2 frame header on the wire, driving the partial-read
  branch in the underlying http2_client.

- NonOkGrpcStatusTrailerDispatchesGrpcErrorBranch
  new grpc_reply_mode::echo_unary_error_status emits the same
  HEADERS+DATA reply as echo_unary but the trailing HEADERS frame
  carries grpc-status: 14 (UNAVAILABLE) and a grpc-message entry,
  driving the grpc_status != ok dispatch in call_raw.

- TruncateAtNineDropsResponsePayloadFailingCallRaw
  injection_mode::truncate with truncate_at=9 is a no-op for the
  9-byte SETTINGS frames so the handshake completes, but the longer
  response HEADERS / DATA / trailers frames lose their payloads;
  call_raw resolves with an error.

- SlowWriteServerFramesStillCompleteHandshake (gated outside
  coverage build) drives the partial-read accumulator branch by
  pacing the server SETTINGS write byte-by-byte.

Substrate addition (tests/support only):
  grpc_reply_mode::echo_unary_error_status — emits trailers with
  grpc-status: 14 + grpc-message: peer-unavailable. Pure
  test-support extension; no production source files modified.

Acceptance: 5+ TEST_F cases covering the issue's required injection
modes (drop/malform/truncate/non-OK trailer/slow_write).

Part of #953
Closes #1107
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant