test(support): add frame_injector for protocol-aware fault injection (Phase 2E of #1074)#1105
Merged
Merged
Conversation
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
Contributor
Coverage Report
Coverage DetailsFull HTML report is available as a build artifact. |
This was referenced May 6, 2026
Closed
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
This was referenced May 6, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 composeswith every loopback peer / friend-test probe shipped in Phases 2A-2D.
Change Type
Affected Components
tests/support/frame_injector.htests/support/frame_injector.cpptests/support/mock_h2_server_peer.{h,cpp}tests/support/mock_grpc_server_peer.{h,cpp}tests/support/mock_quic_peer_loop.{h,cpp}transform()around thesend_toof the Initial datagramtests/support/CMakeLists.txtframe_injector.cppto the static librarytests/CMakeLists.txtnetwork_frame_injector_demo_testtests/unit/frame_injector_demo_test.cpptests/support/README.mdWhy
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, andwebsocket_server.cppabove the >=80 % line /frame_injectoris intentionally minimal: five modes, three of which(
drop,truncate,malform) are pure byte-level transforms; one(
slow_write) only meaningful at write time; andnonefor opt-outbackward compatibility. The default
injection_spec{}produces aninjection_mode::noneinjector, which makes the new constructorargument on each peer a no-op for every existing call site — no Phase
2A-2D test needed any modification.
Who
this PR unblocks them.
When
Where
src/is not touched. No public-API change. Nobuild-config change for production code. The new injector header
pulls in only
<asio/write.hpp>and the standard library, so thenetwork_test_supportlibrary compile cost is unaffected.How
Implementation
frame_injectorcaptures oneinjection_specand exposes two entrypoints:
transform(span)— pure byte transform; returnsoptional<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_topath.write(SyncStream&, span, ec)— template wrapper aroundasio::writethat applies the spec at write time. Used by the h2 / gRPC peers'
TLS-stream writes and natively supports
slow_writebyte-level pacing.Each peer takes the spec via an optional last constructor argument so
existing callers compile unchanged. Internally each
asio::writesitebecomes one
injector_.write(...)call; the QUIC peer wraps the singlesend_tosite in atransform()-then-send_totwo-step.Demo tests
tests/unit/frame_injector_demo_test.cppadds four TEST_F (one perprotocol family), each driving one previously-unreachable error class:
dropmalform(offset 3)malform(offset 0)slow_writews_server_probe::invoke_handle_new_connectionLocal verification
network_test_supportlibrary: builds clean with the new sourcesand the modified peers.
network_frame_injector_demo_test: builds clean and all 4 TEST_Fpass (10 090 ms total — the two that exercise the connect-timeout
branch are bound by the deliberate 500 ms client timeout each).
binaries): builds clean. Five binaries pass identically. One
pre-existing fail in
grpc_client_branch_testand one inquic_socket_branch_testreproduce ondeveloptip with no Phase2E 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_specconstructor argument defaults to{}(==
injection_mode::none) on every peer, so all Phase 2A-2Dcall sites compile and run unchanged.
Rollback plan
Revert this PR. No artifact persists outside
tests/support/andtests/unit/frame_injector_demo_test.cpp.Related
Checklist
tests/support/styleframe_injector_demo_test.cpp)tests/support/README.mdPhase 2E section)