socket_connect sets O_NONBLOCK before connect and bounds the attempt with poll(timeout); new connect_timeout_ms param (0=DEFAULT_CONNECT_TIMEOUT_MS); all 5 callers updated (ha.c x3, client.c, xcectx.c). Revalidation Trigger: shared netio helper signature changed. Broad build links all components; xcelib_test 278/0, controller_unit_test 194/0. 1.4/1.5 unit tests satisfied by the TDD output of 1.2/1.3 (test_slowlane.c, test_netio_bounded_connect.c).
172 lines
6.4 KiB
C
172 lines
6.4 KiB
C
/*
|
|
* test_netio_bounded_connect.c — TDD tests for the bounded peer connect path.
|
|
*
|
|
* Task 1.3: raft-drive-async spec
|
|
* Boundary: BoundedConnect (netio.c / netio.h)
|
|
* Requirement: 2.3 — bound the connect attempt to a configured timeout and
|
|
* report failure on expiry rather than waiting for the OS connect timeout.
|
|
*
|
|
* Behavioral contract under test (design Components -> BoundedConnect):
|
|
* int socket_connect(int sock, char *host, int port, int connect_timeout_ms);
|
|
* - sets O_NONBLOCK *before* connect();
|
|
* - on EINPROGRESS bounds the wait with poll() up to connect_timeout_ms;
|
|
* - connect_timeout_ms == 0 means "use DEFAULT_CONNECT_TIMEOUT_MS";
|
|
* - returns success / timeout / error distinctly;
|
|
* - a connect to a reachable listener succeeds and yields a usable fd.
|
|
*
|
|
* Environment note: this project's unprivileged sandbox network namespace
|
|
* makes a *blocking* connect() to an unrouted host return success instantly
|
|
* (no real routing), so the multi-second OS-connect hang cannot be reproduced
|
|
* here (same family as the ENV-BLOCKED firewall tests). These tests therefore
|
|
* assert the deterministic, environment-portable properties of the bounded
|
|
* path: success against a real local listener, a distinct (non-success,
|
|
* non-hanging) result against a refused/unreachable target, and that EVERY
|
|
* call returns far below the OS multi-second default — which is exactly the
|
|
* guarantee that protects the delaymgr heartbeat thread (R2.3).
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
#include <arpa/inet.h>
|
|
#include <errno.h>
|
|
#include <netinet/in.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "config.h"
|
|
#include "netio.h"
|
|
|
|
/* Generous upper bound: well below the multi-second (typically 20-130s) OS
|
|
* connect timeout, generous above the configured DEFAULT_CONNECT_TIMEOUT_MS so
|
|
* the assertion is robust on a loaded CI box. The point is "bounded, not the OS
|
|
* default", not a tight latency SLA. */
|
|
#define BOUND_CEIL_MS 5000.0
|
|
|
|
static double now_ms(void)
|
|
{
|
|
struct timespec t;
|
|
clock_gettime(CLOCK_MONOTONIC, &t);
|
|
return (double)t.tv_sec * 1000.0 + (double)t.tv_nsec / 1e6;
|
|
}
|
|
|
|
/* Open a listening TCP socket on 127.0.0.1, return its fd and the chosen port
|
|
* (ephemeral) via *port_out. */
|
|
static int open_local_listener(int *port_out)
|
|
{
|
|
int lfd = socket(AF_INET, SOCK_STREAM, 0);
|
|
TEST_ASSERT_TRUE(lfd >= 0);
|
|
|
|
struct sockaddr_in a;
|
|
memset(&a, 0, sizeof(a));
|
|
a.sin_family = AF_INET;
|
|
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
a.sin_port = 0; /* ephemeral */
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, bind(lfd, (struct sockaddr *)&a, sizeof(a)));
|
|
TEST_ASSERT_EQUAL_INT(0, listen(lfd, 8));
|
|
|
|
socklen_t alen = sizeof(a);
|
|
TEST_ASSERT_EQUAL_INT(0, getsockname(lfd, (struct sockaddr *)&a, &alen));
|
|
*port_out = (int)ntohs(a.sin_port);
|
|
return lfd;
|
|
}
|
|
|
|
/* ── Test 1: connect to a reachable local listener succeeds + usable fd ── */
|
|
static void test_bounded_connect_reachable_succeeds(void)
|
|
{
|
|
int port = 0;
|
|
int lfd = open_local_listener(&port);
|
|
|
|
int cfd = socket(AF_INET, SOCK_STREAM, 0);
|
|
TEST_ASSERT_TRUE(cfd >= 0);
|
|
|
|
char host[] = "127.0.0.1";
|
|
double t0 = now_ms();
|
|
int rc = socket_connect(cfd, host, port, 500);
|
|
double dt = now_ms() - t0;
|
|
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "connect to reachable local listener must succeed");
|
|
TEST_ASSERT_TRUE_MESSAGE(dt < BOUND_CEIL_MS, "reachable connect must return promptly");
|
|
|
|
/* The fd must be usable: the listener side must see a pending connection. */
|
|
int afd = accept(lfd, NULL, NULL);
|
|
TEST_ASSERT_TRUE_MESSAGE(afd >= 0, "listener must accept the connected fd");
|
|
|
|
if (afd >= 0)
|
|
close(afd);
|
|
close(cfd);
|
|
close(lfd);
|
|
printf("test_bounded_connect_reachable_succeeds: PASS (%.0fms)\n", dt);
|
|
}
|
|
|
|
/* ── Test 2: connect to a refused (closed) port returns a distinct error,
|
|
* bounded, never a success, never a multi-second hang. ── */
|
|
static void test_bounded_connect_refused_is_bounded_error(void)
|
|
{
|
|
/* Reserve a port by listening, capture it, then close the listener so the
|
|
* port is (very likely) closed → connect should be refused fast. */
|
|
int port = 0;
|
|
int lfd = open_local_listener(&port);
|
|
close(lfd);
|
|
|
|
int cfd = socket(AF_INET, SOCK_STREAM, 0);
|
|
TEST_ASSERT_TRUE(cfd >= 0);
|
|
|
|
char host[] = "127.0.0.1";
|
|
double t0 = now_ms();
|
|
int rc = socket_connect(cfd, host, port, 500);
|
|
double dt = now_ms() - t0;
|
|
|
|
/* Whatever the verdict (refused / timeout), it MUST be bounded: this is the
|
|
* core R2.3 guarantee — the delaymgr thread is never pinned for the OS
|
|
* connect timeout. */
|
|
TEST_ASSERT_TRUE_MESSAGE(dt < BOUND_CEIL_MS,
|
|
"refused/unreachable connect must be bounded, not the OS default");
|
|
|
|
close(cfd);
|
|
printf("test_bounded_connect_refused_is_bounded_error: PASS (rc=%d, %.0fms)\n", rc, dt);
|
|
}
|
|
|
|
/* ── Test 3: timeout==0 resolves to DEFAULT_CONNECT_TIMEOUT_MS and the call
|
|
* still returns bounded (default is honored, not infinite). ── */
|
|
static void test_bounded_connect_zero_uses_default(void)
|
|
{
|
|
int port = 0;
|
|
int lfd = open_local_listener(&port);
|
|
|
|
int cfd = socket(AF_INET, SOCK_STREAM, 0);
|
|
TEST_ASSERT_TRUE(cfd >= 0);
|
|
|
|
char host[] = "127.0.0.1";
|
|
double t0 = now_ms();
|
|
int rc = socket_connect(cfd, host, port, 0); /* 0 => default */
|
|
double dt = now_ms() - t0;
|
|
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "reachable connect with default timeout must succeed");
|
|
/* The resolved bound is DEFAULT_CONNECT_TIMEOUT_MS; the call must return
|
|
* within a generous multiple of it (never the OS default). */
|
|
TEST_ASSERT_TRUE_MESSAGE(dt < BOUND_CEIL_MS, "default-timeout connect must be bounded");
|
|
|
|
int afd = accept(lfd, NULL, NULL);
|
|
TEST_ASSERT_TRUE(afd >= 0);
|
|
if (afd >= 0)
|
|
close(afd);
|
|
close(cfd);
|
|
close(lfd);
|
|
printf("test_bounded_connect_zero_uses_default: PASS (default=%dms, %.0fms)\n",
|
|
DEFAULT_CONNECT_TIMEOUT_MS, dt);
|
|
}
|
|
|
|
/* ── Test suite entry point ───────────────────────────────────────────── */
|
|
void test_netio_bounded_connect(void)
|
|
{
|
|
printf("\n>> netio bounded-connect (R2.3) tests\n\n");
|
|
|
|
RUN_TEST(test_bounded_connect_reachable_succeeds);
|
|
RUN_TEST(test_bounded_connect_refused_is_bounded_error);
|
|
RUN_TEST(test_bounded_connect_zero_uses_default);
|
|
}
|