Skip to content

test(http2): stabilize coverage and expand dispatcher branches (#1106)#1109

Merged
kcenon merged 2 commits into
developfrom
feat/issue-1106-http2-client-r3-coverage-stabilization
May 6, 2026
Merged

test(http2): stabilize coverage and expand dispatcher branches (#1106)#1109
kcenon merged 2 commits into
developfrom
feat/issue-1106-http2-client-r3-coverage-stabilization

Conversation

@kcenon

@kcenon kcenon commented May 6, 2026

Copy link
Copy Markdown
Owner

What

Summary

Round 3 follow-up to PR #1108 (Round 2 of issue #1106). Two changes:

  1. Guard 2 coverage-flaky tests in tests/unit/http2_client_branch_test.cpp (SlowWriteServerSettingsStillCompletesHandshake, EchoOneTruncateAtNineDropsResponsePayloadFailingGet) with #ifndef NETWORK_COVERAGE_BUILD. tests/CMakeLists.txt defines that macro on the branch-test target only when ENABLE_COVERAGE=ON, so Debug/Release matrix runs continue to exercise the slow_write partial-read accumulator and truncate-on-connected request-error branches, while the coverage workflow no longer surfaces them as measured-failing tests.

  2. Add 7 dispatcher TEST_F cases that drive previously-unreachable handlers in process_frame by writing server-originated frames after the SETTINGS handshake. Uses a new post_handshake_frames hook on mock_h2_server_peer (Phase 2E.R3 substrate, first commit) that bypasses the injector so production frame_class::serialize() output reaches the client unmodified.

Change Type

  • Test only (no src/ changes)
  • Feature
  • Bugfix

Affected Components

Path Lines Kind
tests/support/mock_h2_server_peer.h +18 / -2 Constructor parameter + private member
tests/support/mock_h2_server_peer.cpp +24 / -4 Initializer + post-handshake write loop
tests/CMakeLists.txt +12 NETWORK_COVERAGE_BUILD guard for branch-test target
tests/unit/http2_client_branch_test.cpp +263 7 new TEST_F + ifdef wrap on 2 existing

Why

After PR #1108 merged, the post-merge coverage run 25449908024 at b4692fed showed http2_client.cpp line/branch coverage unchanged at 18.8% / 9.9% (target 60% / 40%). Issue #1106 was therefore left OPEN with a recommendation comment proposing exactly the two changes shipped here:

  1. Stabilise the two positive-path TEST_F under coverage instrumentation (option B: NETWORK_COVERAGE_BUILD ifdef).
  2. Add a fault-injection test specifically targeting process_frame dispatch — currently no test in http2_client_branch_test.cpp exercises the connected-state frame-by-frame branch space (handle_window_update, handle_rst_stream, handle_ping, handle_goaway).

Why these specific dispatcher tests

The 4 negative-assertion tests added in PR #1108 (drop / truncate / malform-type / malform-streamid) all assert EXPECT_FALSE(is_connected()) and exercise only early-connect lines that pre-existing happy-path tests already hit. Net coverage contribution: zero. The 2 positive-assertion tests fail under coverage instrumentation, also contributing zero.

The TEST_F batch added here advances the client past the connected state via the unmodified happy-path SETTINGS handshake (which works under coverage instrumentation) and then has the peer write one precisely-formed server-originated frame. Each frame routes through process_frame to a specific handler:

TEST_F Frame Handler / branch driven
ServerPingFrameDrivesHandlePingAndKeepsConnectionAlive PING (ack=0) handle_ping_frame non-ACK path + send_frame reply
ServerPingAckFrameIsAbsorbedSilently PING (ack=1) handle_ping_frame is_ack early-return
ServerGoawayFrameFlipsConnectionStateToDisconnected GOAWAY handle_goaway_frame + is_connected() conjunction
ServerWindowUpdateOnConnectionStreamExpandsWindow WINDOW_UPDATE (sid=0) handle_window_update_frame connection-level branch
ServerWindowUpdateOnUnknownStreamIsSilentlyIgnored WINDOW_UPDATE (sid=99) stream-not-found else branch
ServerRstStreamOnUnknownStreamIsSilentlyIgnored RST_STREAM (sid=99) unknown-stream silent-ignore
ServerUnknownFrameTypeIsHandledWithoutCrashing type=0xFF, len=0 process_frame default branch OR run_io read-error path

Where

  • Test-only scope. src/ is not touched. No public-API change. Production frame classes (ping_frame, goaway_frame, window_update_frame, rst_stream_frame) are reused from internal/protocols/http2/frame.h so wire format always matches what frame::parse expects.
  • The new TEST_F batch sits below the Phase 2E.R2 group under a dedicated section header marked Phase 2E.R3 (Issue #1106).

How

Substrate change (commit 1)

mock_h2_server_peer gains a fourth constructor argument:

explicit mock_h2_server_peer(
    asio::io_context& io,
    reply_mode mode = reply_mode::drain_only,
    injection_spec inject = {},
    std::vector<std::vector<std::uint8_t>> post_handshake_frames = {});

Each entry is written verbatim immediately after settings_exchanged_ flips true and before the per-mode branch. The injector is bypassed (this hook is for tests that need exact wire format, not fault injection). All existing call sites compile unchanged because the new parameter defaults to empty.

Test additions (commit 2)

Each new TEST_F follows the same pattern: serialize a single server-originated frame via the production class, construct the peer with that frame in post_handshake_frames, complete the SETTINGS handshake via make_connected_client, wait briefly for run_io to consume the frame, and assert the resulting state.

Local verification

Build Result
network_test_support library Clean (mock_h2_server_peer.cpp recompiled, +24 / -4)
network_http2_client_branch_test target Clean (no warnings)
18 non-hermetic TEST_F (regression check) All PASS
9 hermetic TEST_F (the 7 new + 2 ifdef'd existing) All FAIL with the same symptom as the unmodified ConnectCompletesSettingsExchangeWithMockPeer baseline test (peer.settings_exchanged() does not flip true within the wait budget on this machine). This is the same pre-existing local-environment hermetic instability documented in PR #1108's body. CI Linux + GitHub macOS runners are the authoritative verification surface.

Test plan for reviewers

cmake --build build --target network_http2_client_branch_test -j 4
./build/bin/network_http2_client_branch_test \
  --gtest_filter="Http2ClientHermeticTransportTest.Server*:Http2ClientHermeticTransportTest.SlowWrite*:Http2ClientHermeticTransportTest.EchoOneTruncate*"
# CI expectation: 9/9 PASS under Debug/Release; 7/7 PASS under coverage
# (the 2 ifdef'd tests are excluded from coverage builds by design)

Acceptance follow-up (#1106)

Coverage targets (line >= 60%, branch >= 40% on http2_client.cpp) are measured by the post-merge coverage workflow on develop. This PR's CI verifies test correctness; the coverage delta will appear on the next develop coverage run after merge. Whether one PR can close the gap entirely or whether a Round 4 is needed will be determined from that measurement, following the Round 1 / Round 2 convention of keeping #1106 open until coverage is verified.

Breaking changes

None. The mock_h2_server_peer constructor change is additive with a default value.

Rollback plan

Revert this PR. Production code is not touched. The substrate change is independent of any caller, so no other tests need adjusting.

Related

Checklist

kcenon added 2 commits May 7, 2026 06:35
…_peer

Adds an optional post_handshake_frames constructor argument that lets
callers supply pre-serialized HTTP/2 frame byte buffers for the mock
peer to write immediately after the SETTINGS-ACK and before the
per-mode request/response handling.

Each entry is written verbatim, bypassing the frame_injector, so tests
can construct precisely-formed PING, GOAWAY, WINDOW_UPDATE, RST_STREAM
or arbitrary unknown-type frames whose wire format frame::parse will
accept and route through the client's process_frame dispatcher.

This unlocks dispatcher-branch coverage paths in http2_client.cpp that
the request-error path (echo_one + injector) cannot reach because
those branches require a server-originated frame on the connected
state, not a corrupted response to a client-initiated request.

Default empty value preserves existing Phase 2A / 2A.2 / 2E.R2
behavior for all current call sites.
Phase 2E.R3 follow-up to PR #1108. The Round 2 work landed 6 new TEST_F
cases on the http2_client.cpp branch test, but the post-merge coverage
workflow showed line/branch coverage on http2_client.cpp unchanged at
18.8% / 9.9% (target 60% / 40%).

Root cause: 2 positive-path TEST_F (slow_write handshake, echo_one +
truncate-at-9 response) fail under coverage instrumentation because
lcov/gcov slow the SETTINGS handshake past the 2-3 second wait budget.
The other 4 TEST_F pass but only re-cover early-connect lines that
existing happy-path tests already hit, so they contribute zero net
coverage. handle_ping_frame, handle_goaway_frame,
handle_window_update_frame, handle_rst_stream_frame, and process_frame
default branch remain unreachable.

Two changes:

1. Guard the two coverage-flaky tests with #ifndef NETWORK_COVERAGE_BUILD.
   tests/CMakeLists.txt defines NETWORK_COVERAGE_BUILD on the branch
   test target only when ENABLE_COVERAGE is on, so Debug/Release matrix
   builds keep the slow_write partial-read accumulator and truncate-on-
   connected request-error branches under test, while the coverage
   workflow no longer carries a measured-failing-test signal.

2. Add 7 dispatcher TEST_F cases that use the new
   mock_h2_server_peer post_handshake_frames hook to write
   server-originated frames after the SETTINGS handshake:

   - ServerPingFrameDrivesHandlePingAndKeepsConnectionAlive
   - ServerPingAckFrameIsAbsorbedSilently
   - ServerGoawayFrameFlipsConnectionStateToDisconnected
   - ServerWindowUpdateOnConnectionStreamExpandsWindow
   - ServerWindowUpdateOnUnknownStreamIsSilentlyIgnored
   - ServerRstStreamOnUnknownStreamIsSilentlyIgnored
   - ServerUnknownFrameTypeIsHandledWithoutCrashing

   Each frame is serialized via the production frame class
   (ping_frame, goaway_frame, window_update_frame, rst_stream_frame)
   so frame::parse accepts the wire format and routes the frame to
   the intended handler. The unknown-type case constructs raw bytes
   with type=0xFF to drive either process_frame's default branch or
   the run_io read-error path; both outcomes are RFC-compliant.

Local Debug build: existing pre-merge hermetic instability on macOS
Apple Silicon affects the new tests the same way it affects the
unmodified ConnectCompletesSettingsExchangeWithMockPeer baseline
(documented in PR #1108 body). CI Linux + GitHub macOS runners are
the authoritative verification surface, as before.

Closes #1106
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Metric Value
Line Coverage 69.5%
Branch Coverage 34.5%
Target 80% lines / 70% branches
Coverage Details

Full HTML report is available as a build artifact.

@kcenon kcenon merged commit d537864 into develop May 6, 2026
15 checks passed
@kcenon kcenon deleted the feat/issue-1106-http2-client-r3-coverage-stabilization branch May 6, 2026 22:19
kcenon added a commit that referenced this pull request May 8, 2026
…ixture (#1113)

Scale every hermetic-fixture wait_for budget by NETWORK_COVERAGE_TIMEOUT_MULTIPLIER
so Http2ClientHermeticTransportTest dispatcher cases can complete the SETTINGS
handshake under -fprofile-arcs -ftest-coverage instrumentation. PR #1111
diagnosis confirmed all 7 Round-3 dispatcher TEST_F (and ~30 other fixture-based
tests) failed at wait_for(settings_exchanged(), 3s): 5.1-5.4s observed under
coverage vs 100-300ms in Debug/Release, producing 0pp delta on
src/protocols/http2/http2_client.cpp for three rounds (PR #1108, PR #1109).

Changes:
- tests/support/hermetic_transport_fixture.h: define
  NETWORK_COVERAGE_TIMEOUT_MULTIPLIER (default 1), apply to wait_for default
  timeout argument.
- tests/support/hermetic_transport_fixture.cpp: scale make_loopback_tcp_pair
  accept-handler deadline by the multiplier.
- tests/support/CMakeLists.txt: define
  NETWORK_COVERAGE_TIMEOUT_MULTIPLIER=5 on network_test_support PUBLIC when
  ENABLE_COVERAGE is ON, so all consuming test executables inherit it at the
  point of header inclusion.

No src/ changes. No new TEST_F. Multiplier defaults to 1 outside coverage
builds; Debug/Release matrix observable behaviour is unchanged.

Closes #1112
kcenon added a commit that referenced this pull request May 8, 2026
Records the Round 6 friend-injected process_frame pivot for #1115/#1116
under [Unreleased] in both root CHANGELOG.md and docs/CHANGELOG.md (SSOT).

Notes the load-bearing 15.175s -> 0ms wall-time collapse, the 7 Server*
TEST_F scope vs the 14 connect-state TEST_F that remain on the multiplier
scaffold, the production-header byte-identity guarantee under BUILD_TESTS=OFF,
and the deferred post-merge coverage gate (>=+20pp line / >=+10pp branch
vs PR #1109 baseline) on src/internal/protocols/http2/http2_client.cpp.
kcenon added a commit that referenced this pull request May 8, 2026
Records the Round 6 friend-injected process_frame pivot for #1115/#1116
under [Unreleased] in both root CHANGELOG.md and docs/CHANGELOG.md (SSOT).

Notes the load-bearing 15.175s -> 0ms wall-time collapse, the 7 Server*
TEST_F scope vs the 14 connect-state TEST_F that remain on the multiplier
scaffold, the production-header byte-identity guarantee under BUILD_TESTS=OFF,
and the deferred post-merge coverage gate (>=+20pp line / >=+10pp branch
vs PR #1109 baseline) on src/internal/protocols/http2/http2_client.cpp.
kcenon added a commit that referenced this pull request May 8, 2026
Closes #1115

Pivot http2_client dispatcher branch tests to friend-injected process_frame
to bypass the SETTINGS-handshake wait_for path. PR #1114 (Round 5 multiplier)
was structurally bounded by the ctest 300 s timeout under coverage build;
direct injection collapses the 15.175 s wall-clock to 0 ms.

- Add http2_client_test_access friend class gated by NETWORK_ENABLE_TEST_INJECTION
- Refactor 7 Server* dispatcher TEST_F to call process_frame directly
- Production header byte-identical when BUILD_TESTS=OFF
- 14 connect-state TEST_F genuinely require connection state, retained on multiplier scaffold

Post-merge verification: Coverage Analysis workflow on develop measures
delta vs PR #1109 baseline (LH/LF=108/576, BRH/BRF=97/979). Acceptance gate
per #1115 is >=+20pp line / >=+10pp branch on http2_client.cpp.

Part of #953 coverage expansion.
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