/* * 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 #include #include #include #include #include #include #include #include #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); }