Skip to content

test(websocket): expand branch coverage for websocket_server.cpp#1059

Merged
kcenon merged 1 commit into
developfrom
test/issue-1053-websocket-server-coverage
Apr 26, 2026
Merged

test(websocket): expand branch coverage for websocket_server.cpp#1059
kcenon merged 1 commit into
developfrom
test/issue-1053-websocket-server-coverage

Conversation

@kcenon

@kcenon kcenon commented Apr 26, 2026

Copy link
Copy Markdown
Owner

What

Summary

Add branch coverage tests for src/http/websocket_server.cpp exercising public-API surfaces of messaging_ws_server that remained uncovered after Issue #989 baseline measurement (line 39.9% / branch 19.7% on develop @ 05c1b7b, 2026-04-26). All 69 test cases across 10 test suites are hermetic and operate purely on the public API; no live TCP peer or WebSocket handshake is required.

Change Type

  • Test

Why

Related Issues

Motivation

src/http/websocket_server.cpp sits among the lowest-coverage files in the network_system source tree. The uncovered region concentrates in error and boundary paths — exactly the surfaces that regress during the ongoing Result<T> migration and API stabilization. Bringing this file over the 70% line / 60% branch bar contributes directly to the #953 acceptance criteria. This PR completes the 5-issue sequential batch of branch-coverage sub-issues under epic #953 (sibling PRs: #1054 http2_client, #1055 grpc_client, #1056 http2_server, #1057 quic_socket, #1058 quic_server).

Where

Files Changed

  • tests/unit/websocket_server_branch_test.cpp (new, ~976 lines, 69 test cases)
  • tests/CMakeLists.txt (registration entry mirroring network_quic_server_branch_test)
  • CHANGELOG.md (Unreleased > Added entry)
  • docs/CHANGELOG.md (Unreleased > Added detailed entry)

How

Implementation Highlights

Test groups (suite names):

  • WsServerConfigRoundTrip (11 tests) — ws_server_config defaults, port / max_connections / ping_interval / max_message_size boundary values (zero / one / max), empty / 1024-char / binary-byte path values, auto_pong toggle, full-config copy and assignment round-trips
  • WsServerConstruction (5 tests) — empty / 512-char / binary-byte server IDs, 16-iteration short-lived construction, server_id() reference stability, std::string_view literal constructor variant
  • WsServerDefaults (6 tests) — is_running()==false, connection_count()==0, empty get_all_connections(), get_connection() returning nullptr for unknown / empty / 2048-char / binary-byte ids, repeated default-state stability
  • WsServerStop (4 tests) — stop_server() / i_websocket_server::stop() returning ok() on the prepare_stop() early-return branch when never started, repeated stop idempotency, is_running() invariance
  • WsServerBroadcast (8 tests) — broadcast_text() and broadcast_binary() session_mgr_ == nullptr early-return for empty / small / 64 KiB payloads with binary-byte text strings and 4-iteration repeated invocation
  • WsServerInterfaceCallbacks (16 tests) — every i_websocket_server callback adapter (connection / disconnection / text / binary / error) accepting null callbacks (empty-function branch), populated lambdas (wrap-and-store branch), triple replacement, combined registration, shared_ptr-captured state, and null-then-populated-then-null toggle exercising the if-callback / else-empty branch in every adapter twice
  • WsServerConcurrency (5 tests) — 4 threads x 200 iterations of is_running() / connection_count() / server_id() / get_all_connections() under shared_ptr lifetime, plus a writer/reader pair on set_error_callback() / is_running()
  • WsServerMultiInstance (3 tests) — 8 servers with independent default state, broadcast-on-one-does-not-affect-another, stop-one-affects-none
  • WsServerTypeAlias (3 tests) — ws_server and secure_ws_server instantiation, interop assignment between them via shared_ptr
  • WsServerDestructor (4 tests) — destructor cleanup on never-started server, after stop, with all five interface callbacks registered, and after broadcast invocation

The test file follows the pattern established by sibling PR #1058 (quic_server_branch_test.cpp). The namespace alias wsiface = kcenon::network::interfaces is used in place of iface to avoid the Linux glibc <net/if.h> collision (struct iface ifa_ifp) that bit PR #1058 mid-CI.

Honest scope statement

The impl-level methods that physically accept TCP connections, complete the WebSocket HTTP/1.1 upgrade handshake, exchange frames with a peer, and dispatch per-message callbacks remain reachable only with a live TCP client that speaks the WebSocket wire format end-to-end. Specifically, the messaging_ws_server private surfaces:

  • do_start_impl() success path past asio::ip::tcp::acceptor construction (the bind_failed branches for address_in_use / access_denied are reachable only by binding to a privileged or in-use port and would not be hermetic; the success path would also leave the io_context loop running asynchronously inside the test process and pollute global state)
  • do_stop_impl() — only reachable after a successful start
  • do_accept() — async TCP accept completion, reachable only with a live io_context::run() loop
  • handle_new_connection() — TCP socket construction + websocket_socket wrapping + ws_connection_impl construction + per-connection callback wiring + ws_socket->async_accept handshake initiation, reachable only after a TCP connect that completes the WebSocket HTTP/1.1 upgrade handshake
  • on_message() — reachable only after async_read of a complete frame
  • on_close() — reachable only after a peer-initiated close frame or local close()
  • on_error() — reachable only after a transport-level error during read/write
  • The four invoke_*_callback() helpers — reachable only from frame-driven paths inside the io_context

The success branches of broadcast_text() / broadcast_binary() with at least one populated session, and the non-null branch of get_connection() for a known id, also require a live WebSocket client that completes the handshake, since session_mgr_->add_connection() in handle_new_connection() is the only path that populates the session map and that call sits behind the ws_socket->async_accept callback. The ws_connection_impl private methods (send_text / send_binary / close / is_connected / get_socket / invalidate) are reachable only when at least one connection has been added through handle_new_connection() and so are also blocked by the same handshake requirement.

Testing those branches hermetically would require either a mock thread pool that does not actually run io_context::run, a friend-declared injection point inside messaging_ws_server, or a transport fixture that drives the WebSocket upgrade handshake from a test-side client.

Testing Done

  • Tests added: 69 test cases in 10 test suites
  • Local build: skipped — cmake/ctest not installed in sandbox
  • Will rely on CI

Breaking Changes

None — test-only additions.

Add tests/unit/websocket_server_branch_test.cpp exercising public-API
surfaces of messaging_ws_server that remained uncovered after Issue #989
baseline measurement (line 39.9% / branch 19.7%).

Coverage targets:
- ws_server_config full-field round-trip (port / max_connections /
  ping_interval / max_message_size at zero / 1 / max boundaries, path
  with empty / 1024-char / binary-byte values, auto_pong toggle)
- messaging_ws_server construction with empty / 512-char / binary-byte
  server IDs and string_view literal variant
- Default-state queries on never-started server (is_running,
  connection_count, get_connection, get_all_connections)
- stop_server() / i_websocket_server::stop() returning ok() on
  prepare_stop() early-return branch
- broadcast_text() / broadcast_binary() session_mgr_ == nullptr
  early-return for empty / small / 64 KiB payloads
- Interface (i_websocket_server) callback adapters for connection /
  disconnection / text / binary / error with null callbacks (empty-
  function branch), populated lambdas, triple replacement, shared_ptr-
  captured state, and null-then-populated-then-null toggle
- Concurrent state queries and callback replacement under shared_ptr
- Multi-instance independent state and type alias coverage
- Destructor cleanup paths

Tests are hermetic: no live TCP peer, no WebSocket handshake. The impl-
level methods that exchange frames remain reachable only with a
transport fixture; this PR documents them in an honest scope statement.

Closes #1053
@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report

Metric Value
Line Coverage 66.6%
Branch Coverage 32.9%
Target 80% lines / 70% branches
Coverage Details

Full HTML report is available as a build artifact.

@kcenon kcenon merged commit e78f0f7 into develop Apr 26, 2026
9 checks passed
@kcenon kcenon deleted the test/issue-1053-websocket-server-coverage branch April 26, 2026 14:32
kcenon added a commit that referenced this pull request Apr 27, 2026
* test(websocket): expand websocket_server.cpp coverage

Append 33 unit tests to tests/test_messaging_ws_server.cpp targeting
public-API surfaces of messaging_ws_server reachable without a live
WebSocket peer. Complements tests/unit/websocket_server_branch_test.cpp
(added in #1059) by exercising additional angles:

- ws_message struct as_text() / as_binary() round-trip and reference
  semantics used by invoke_message_callback() routing
- ws_close_code enum value invariants per RFC 6455 Section 7.4 used
  by ws_connection::close(uint16_t, string_view) and on_close()
- Constructor variations (c-string, std::string, string_view,
  substring string_view) and server_id() reference stability
- Default-state queries via interfaces::i_websocket_server pointer
  (is_running, connection_count, stop)
- get_connection() lookup for empty / 2048-char / IPv4-style ids on
  never-started server returning nullptr
- get_all_connections() empty-vector stability on never-started server
- broadcast_text / broadcast_binary with registered text / binary
  callbacks but no peers does not invoke the callbacks
- Interface callback adapters: lambda capture is stored not invoked
  immediately for connection / disconnection / error
- All five interface callbacks replaceable with default-constructed
  std::function (covers the empty-function adapter branch)
- Multi-instance state isolation across 32 sequential constructions
- Two servers with the same id are independent instances
- wait_for_stop on never-started server returns under one second
- stop_server idempotency across 10 calls and via interface pointer

These tests are hermetic and rely solely on state reachable without
a live io_context loop or TCP peer. The acceptance criteria of #1067
(line >= 80% / branch >= 70%) cannot be met from unit tests alone
because the post-handshake frame I/O path requires an in-process
WebSocket loopback fixture that does not exist yet; the gap is
tracked under #953.

Part of #1067
Part of #953

* test(websocket): drop protected is_running() call via interface pointer

i_network_component::is_running() is declared protected in the base
interface; only derived class messaging_ws_server publishes it
publicly. Calling is_running() through std::shared_ptr<i_websocket_server>
fails to compile with 'is protected within this context'.

Replace the failing test with an interface-pointer stop()/connection_count()
idempotency variant that uses only public surface.

---------

Co-authored-by: Raphael Shin <raphael.shin@flonics.co.kr>
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