Skip to content

infra(test): add mock_grpc_server_peer for gRPC unary loopback (Phase 2B of #1074)#1102

Merged
kcenon merged 1 commit into
developfrom
feat/issue-1074-mock-grpc-server-peer-phase-2b
May 3, 2026
Merged

infra(test): add mock_grpc_server_peer for gRPC unary loopback (Phase 2B of #1074)#1102
kcenon merged 1 commit into
developfrom
feat/issue-1074-mock-grpc-server-peer-phase-2b

Conversation

@kcenon

@kcenon kcenon commented May 3, 2026

Copy link
Copy Markdown
Owner

What

Summary

Phase 2B of issue #1074: ships mock_grpc_server_peer — a server-side gRPC framing peer composed on top of the existing mock_h2_server_peer's HTTP/2 SETTINGS exchange. With grpc_reply_mode::echo_unary, the peer reads one client request stream and replies with response HEADERS (:status: 200, content-type: application/grpc), one length-prefixed gRPC DATA frame, and trailing HEADERS (grpc-status: 0, END_STREAM).

Change Type

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

Affected Components

  • tests/support/mock_grpc_server_peer.{h,cpp} — new server-side gRPC peer
  • tests/support/CMakeLists.txt — add new TU to network_test_support
  • tests/support/README.md — document the new peer + composition example
  • tests/unit/grpc_client_branch_test.cpp — one demo TEST_F covering the unary success path

Why

Problem Solved

grpc_client::call_raw post-connect success branches (http2_client::post wait, trailer-scan extracting grpc-status, grpc_message::parse on the response body) cannot be exercised by the Phase 2A mock_h2_server_peer because it never replies with HEADERS+DATA+trailers. This blocks branch-coverage expansion on protocols/grpc/client.cpp (#1063 follow-ups) and was explicitly called out in #1074's scope as Phase 2B.

Related Issues

Alternative Approaches Considered

  • Reusing mock_h2_server_peer::echo_one: rejected because gRPC needs the trailer frame (grpc-status: 0) and content-type: application/grpc, neither of which are part of the bare h2 echo path.
  • Pulling in a real gRPC server (grpc++): rejected as a hermeticity violation. Phase 2 specifically requires an in-process loopback peer with no external dependencies beyond the existing ASIO + OpenSSL stack.

Where

Directory Files Type
tests/support/ mock_grpc_server_peer.h, mock_grpc_server_peer.cpp New
tests/support/ CMakeLists.txt Modified
tests/support/ README.md Modified
tests/unit/ grpc_client_branch_test.cpp Modified

API Changes

None. New test-only infrastructure under tests/support/. No changes to src/ or public headers.

How

Implementation Highlights

  1. Same SETTINGS handshake as mock_h2_server_peer: the new peer reuses the wire sequence (preface read -> server SETTINGS -> client SETTINGS read -> SETTINGS-ACK) so behavior diverges only after step 5.
  2. HPACK encoding is dependency-free: :status: 200 uses static index 8 (one byte: 0x88); content-type: application/grpc and grpc-status: 0 use literal-without-indexing (0x00 prefix) with plain non-Huffman length-prefixed strings. All values fit in <=127 bytes so the simple 7-bit length form applies.
  3. Trailer frame ordering: response HEADERS (END_HEADERS, no END_STREAM) -> DATA (no END_STREAM) -> trailing HEADERS (END_HEADERS + END_STREAM). The h2 client appends both header blocks into response_headers, so grpc_client sees :status and grpc-status in the same vector.
  4. Hermetic discipline preserved: bound to 127.0.0.1:0 via the underlying tls_loopback_listener; RSA-2048 cert regenerated per construction; no DNS, no external network, no on-disk secrets.

Testing Done

Local C++ toolchain unavailable in the working environment — relying on CI for build / test verification. Code reviewed for HPACK correctness against src/protocols/http2/hpack.cpp (verified 0x88 indexed and 0x00 literal-without-indexing decode paths) and frame constructors against src/internal/protocols/http2/frame.h.

Test Plan for Reviewers

  1. Build with the standard preset: cmake --preset release && cmake --build build
  2. Run the new demo test: cd build && ctest -R CallRawSucceedsWithMockGrpcPeerEchoUnary --output-on-failure
  3. Verify the test exercises:
    • grpc_client::call_raw returns a successful Result<grpc_message>
    • The response payload is the expected {'o', 'k'} body
    • peer.request_received() and peer.response_sent() both flip true
    • The captured peer.request_body() contains the gRPC 5-byte length prefix the client serialized

Breaking Changes

None.

Rollback Plan

Single commit — revert in one step. No migrations, no data, no public-API surface change.

Issue scope acknowledgement

This PR ships Phase 2B only. Phases 2C (mock_quic_peer_loop), 2D (network_test_friends.h friend-test injection), and 2E (frame_injector) remain. Per the #1074 acceptance criteria the issue stays open until all five phases land plus per-protocol demo tests and the README entries. This PR uses Part of, not Closes.

Part of #1074
Part of #953

… 2B of #1074)

Phase 2B of issue #1074 (protocol-aware loopback peers + error injection).
Adds a server-side gRPC framing peer composed on top of the existing
mock_h2_server_peer's HTTP/2 SETTINGS exchange.

What this ships:

- tests/support/mock_grpc_server_peer.{h,cpp}: same SETTINGS handshake as
  the h2 peer, then with grpc_reply_mode::echo_unary the worker reads one
  client request stream (HEADERS + DATA up to END_STREAM) and replies on
  the same stream with:
    1. response HEADERS (":status: 200", "content-type: application/grpc",
       END_HEADERS, no END_STREAM)
    2. one length-prefixed gRPC DATA frame (5-byte gRPC header + body)
    3. trailing HEADERS ("grpc-status: 0", END_HEADERS + END_STREAM)
  HPACK uses static index 8 for :status: 200 and literal-without-indexing
  for content-type/grpc-status to keep the encoder dependency-free.

- tests/support/CMakeLists.txt: add the new translation unit to the
  network_test_support static library.

- tests/support/README.md: document the new peer in the file table and
  add a copy-paste composition example covering the unary success path.

- tests/unit/grpc_client_branch_test.cpp: one demo TEST_F
  (CallRawSucceedsWithMockGrpcPeerEchoUnary) that drives
  grpc_client::call_raw past the http2_client::post wait, the trailer
  scan that extracts grpc-status, and grpc_message::parse on the
  response body. Covers branches that the Phase 2A peer could not
  reach because it never replied with HEADERS+DATA+trailers.

Hermetic: bound to 127.0.0.1:0 via the underlying tls_loopback_listener;
RSA-2048 cert regenerated per construction; no DNS, no external network,
no on-disk secrets.

Issue scope: this PR delivers Phase 2B only. Phases 2C (mock_quic_peer_loop),
2D (network_test_friends.h injection), and 2E (frame_injector) remain.
Per the #1074 acceptance criteria the issue stays open until all five
phases land plus per-protocol demo tests and the README entries.

Part of #1074
Part of #953
@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

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

Full HTML report is available as a build artifact.

@kcenon kcenon merged commit 568a2d3 into develop May 3, 2026
15 checks passed
@kcenon kcenon deleted the feat/issue-1074-mock-grpc-server-peer-phase-2b branch May 3, 2026 21:01
kcenon added a commit that referenced this pull request May 5, 2026
…2C of #1074) (#1103)

Add server-side QUIC Initial echo peer to tests/support/. The peer
receives one client Initial datagram, derives QUIC v1 initial keys
from the original DCID (RFC 9001 Section 5.2), and replies with a
header-protected server Initial carrying a stub crypto_frame
(type 0x06). This makes quic_socket::process_crypto_frame reachable
from a hermetic test for the first time, unblocking coverage
expansion under #1062 / #1065.

Phase 2A (#1075) and 2B (#1102) shipped HTTP/2 and gRPC peers; this
follows the same RAII shape: shared io_context, dedicated worker
thread, atomic state flags polled via wait_for. Phase 2D (friend-
test injection) and 2E (frame_injector) remain open under #1074.

Files:
- tests/support/mock_quic_peer_loop.{h,cpp} (new)
- tests/support/CMakeLists.txt (+1)
- tests/support/README.md (+1)
- tests/unit/quic_socket_branch_test.cpp (+46) -- one demo TEST_F

Part of #1074
kcenon added a commit that referenced this pull request May 6, 2026
…#1104)

Add friend-test injection so unit tests can drive two private
server-side methods previously unreachable from public APIs:

- messaging_quic_server::handle_packet
  (src/internal/experimental/quic_server.h:432)
- messaging_ws_server::handle_new_connection
  (src/internal/http/websocket_server.h:415)

Mechanism: new compile definition NETWORK_ENABLE_TEST_INJECTION
gated behind BUILD_TESTS in cmake/network_system_targets.cmake.
The two production headers gain a forward declaration block and
a single 'friend class' line, all wrapped in
'#if defined(NETWORK_ENABLE_TEST_INJECTION)'. When BUILD_TESTS=OFF
the production binary is byte-identical to the previous develop tip.

Probe types live entirely in tests/support/:
- network_test_friends.h forward-declares the probes
- quic_server_probe.{h,cpp} forwards to handle_packet
- ws_server_probe.{h,cpp} forwards to handle_new_connection

Demo tests verify end-to-end wiring:
- QuicServerProbeTest.HandlePacket{Empty,Garbage}BufferDoesNotCrash
- WsServerProbeTest.HandleNewConnectionEarlyReturnNoSessionManager
- WsServerProbeTest.HandleNewConnectionEmptySocketEarlyReturn

Per #1074 scope: no behavioral changes to src/. Phases 2A (#1075),
2A.2 (#1082), 2B (#1102), 2C (#1103) merged; 2E remains.

Part of #1074
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