Skip to content

infra(test): pivot to friend-injected process_frame for hermetic dispatcher coverage (Round 6, Part of #1106, #953) #1115

Description

@kcenon

What

Pivot from the caller-side wait_for multiplier approach (Round 5: #1112 + PR #1113 + PR #1114) to a friend-injected process_frame mechanism so that Http2ClientHermeticTransportTest dispatcher TEST_F can exercise src/protocols/http2/http2_client.cpp branches without depending on the async SETTINGS-handshake gate. PR #1111 diagnosis named this as the architectural alternative ("Option C") to be used if the multiplier approach proved inadequate.

Change Type

  • Test infrastructure (no src/ behavioural change beyond a friend declaration; no new TEST_F shape, but existing dispatcher TEST_F refactored to call process_frame directly)

Affected Components

  • src/protocols/http2/http2_client.h — add friend class Http2ClientHermeticTransportTest; (or per-test friend declarations) gated by a test-only macro so production builds are unaffected
  • tests/unit/http2_client_branch_test.cpp — refactor dispatcher TEST_F (~24 cases at the lines edited by PR fix(test): apply NETWORK_COVERAGE_TIMEOUT_MULTIPLIER at explicit wait_for call sites (#1112) #1114) to call process_frame (or whichever internal entry point cleanly exposes dispatcher branches) directly with crafted frames, instead of waiting on wait_for(SETTINGS-exchange)
  • tests/support/hermetic_transport_fixture.{h,cpp} — likely no change; fixture remains for tests that genuinely need async transport
  • Possibly: tests/CMakeLists.txt — define a test-only macro (e.g. NETWORK_HTTP2_CLIENT_FRIEND_TESTS) when building test targets so the friend declaration only activates in test builds

Why

PR #1114 acceptance verification (Coverage Analysis run 25562347620, head 1efbc1d) measured 0pp delta on http2_client.cpp (LH/LF=108/576, BRH/BRF=97/979 — byte-identical with the PR #1109 baseline). Test execution evidence:

ctest # TEST_F Outcome Duration
5867 ConnectAttemptsHandshakeAgainstLoopbackTlsPeer Passed 0.27 s
5868 ConnectCompletesSettingsExchangeWithMockPeer FAILED 15.175 s
5869 SecondConnectReturnsAlreadyExistsAfterFirstSucceeds Timeout 300.01 s (ctest 5-min cap)
5870 SendRequestTimesOutWhenPeerSendsNoHeaders FAILED 15.427 s

Test #5868's 15.175 s exactly equals wait_for(3s) × NETWORK_COVERAGE_TIMEOUT_MULTIPLIER(5) = 15s — confirming the PR #1114 multiplier was applied as designed. SETTINGS handshake under coverage instrumentation now exceeds 15 s (vs the 5.1–5.4 s observed in PR #1111 Round-3 diagnosis), and Test #5869 hitting the ctest 300 s timeout proves that simply increasing the multiplier (10×, 15×) is incompatible with reasonable ctest budgets — the multiplier-scaling approach is structurally bounded.

The dispatcher branches in http2_client.cpp cannot be reached via the async SETTINGS-handshake path under instrumentation. Bypassing that path entirely (Option C) is the remaining architectural avenue.

Related Issues

Where

Path Role
src/protocols/http2/http2_client.h Add friend declaration for the hermetic test fixture, gated by #ifdef NETWORK_HTTP2_CLIENT_FRIEND_TESTS
tests/unit/http2_client_branch_test.cpp Refactor 24 dispatcher TEST_F to call process_frame(...) (or chosen internal entry point) directly with constructed frames; the wait_for(SETTINGS-exchange) step is dropped
tests/CMakeLists.txt Define NETWORK_HTTP2_CLIENT_FRIEND_TESTS=1 PRIVATE on the test binary that consumes the friend access

No other src/ behavioural changes. No new TEST_F. The 24 existing dispatcher TEST_F continue to assert the same observable contract; only their execution path changes.

How

Technical Approach

  1. Choose the friend entry point. Inspect Http2Client for a single internal method that already drives the dispatcher (likely something like process_frame, handle_frame, or dispatch_frame). The chosen entry point should be reachable without first completing SETTINGS handshake — this is the whole point of Option C.
  2. Add the friend declaration in src/protocols/http2/http2_client.h:
    #ifdef NETWORK_HTTP2_CLIENT_FRIEND_TESTS
    class Http2ClientHermeticTransportTest;  // forward-declare in test namespace
    class Http2Client {
        // ...
        friend class ::testing::Test;  // or a more specific friend class
        // ...
    };
    #endif
    Verify the exact friend syntax against the test class's actual namespace.
  3. Refactor the 24 dispatcher TEST_F in tests/unit/http2_client_branch_test.cpp. Before:
    TEST_F(Http2ClientHermeticTransportTest, ServerPingFrameDrivesHandlePingAndKeepsConnectionAlive) {
        auto client = make_connected_client();  // hangs at SETTINGS handshake under coverage
        peer_->send_ping_frame();
        ASSERT_TRUE(wait_for([&]{ return client->is_alive(); }, 3s));
    }
    After:
    TEST_F(Http2ClientHermeticTransportTest, ServerPingFrameDrivesHandlePingAndKeepsConnectionAlive) {
        auto client = make_uninitialized_client();  // skip handshake; client constructor only
        Http2Frame frame = build_ping_frame();
        client->process_frame(frame);  // direct dispatcher call via friend access
        EXPECT_TRUE(client->ping_handler_invoked());  // or appropriate assertion
    }
  4. Define the test-only macro in tests/CMakeLists.txt:
    target_compile_definitions(network_http2_client_branch_test PRIVATE NETWORK_HTTP2_CLIENT_FRIEND_TESTS=1)
  5. Do NOT remove the existing wait_for/multiplier scaffolding — it stays for tests that genuinely exercise async transport (e.g. ConnectAttemptsHandshakeAgainstLoopbackTlsPeer which already passes in 0.27 s). Only the dispatcher-branch TEST_F migrate to direct-call.

Acceptance Criteria

  • src/protocols/http2/http2_client.h defines a friend access point gated by NETWORK_HTTP2_CLIENT_FRIEND_TESTS; production builds (where the macro is undefined) are byte-identical (verified by ABI/hash check or simple diff of preprocessor output).
  • All 24 dispatcher TEST_F in tests/unit/http2_client_branch_test.cpp execute the dispatcher branch via direct process_frame (or chosen entry point) instead of wait_for(SETTINGS-exchange).
  • Debug and Release matrix CI on the PR pass (multiplier=1 path unchanged for the few remaining wait_for-based tests; friend access compiles cleanly).
  • Post-merge coverage workflow run on develop measures delta ≥ +20pp line AND ≥ +10pp branch on src/protocols/http2/http2_client.cpp versus the pre-merge sha. Reference baseline: 18.75% line / 9.91% branch (108/576, 97/979) from PR test(http2): stabilize coverage and expand dispatcher branches (#1106) #1109.
  • No dispatcher TEST_F hits the ctest 300 s timeout (vs Test #5869's timeout in run 25562347620).
  • No new TEST_F introduced; only execution-path refactor.

Test Plan for Reviewers

  1. Inspect the friend-declaration diff and confirm production builds are unaffected (e.g. cmake --build in non-test mode does not see the friend).
  2. Inspect the refactored TEST_F bodies — confirm they call the chosen dispatcher entry point directly and assert observable side-effects.
  3. After merge, trigger the develop coverage workflow and check coverage_filtered.info for post-merge LH/LF/BRH/BRF on http2_client.cpp against the ≥+20pp / ≥+10pp gate.

Breaking Changes

None. friend declarations gated by a test-only macro do not affect production binary layout (some compilers emit different symbol info for friends, but no observable runtime/ABI change). The 24 TEST_F still assert the same contracts.

Rollback

git revert is sufficient. Test infrastructure + a single header friend line; no production-source semantics change.

Risks

  • Wrong entry point choice: if process_frame requires connection state that only the SETTINGS-handshake provides, calling it on an uninitialized client may segfault or hit asserts. Mitigation: read the dispatcher implementation carefully before choosing the entry point; if needed, add a setup_minimal_state_for_testing() helper exposed via friend.
  • Friend coupling: introducing friend class Http2ClientHermeticTransportTest couples production header to a test class name. Mitigation: use a single dedicated friend struct (e.g. struct Http2ClientTestAccess) in the production header, and have the test class delegate through that. This keeps the production header agnostic of test class names.
  • Coverage delta still 0pp: if the dispatcher branches truly cannot be reached without going through process_frame's prerequisites that themselves call into branch-counted paths, Option C also fails and the coverage gap is structural. In that case, the right move is to lower the gate or accept the gap with a written justification (separate decision).

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions