From e5fafacf9967069a659ab59a82d5f19cffa914a7 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 21 Oct 2026 16:45:53 +0200 Subject: [PATCH] Cover TCP timer replacement failures and pending FIN cleanup --- tests/Makefile.am | 3 + tests/test_tcp_io.c | 150 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/tests/Makefile.am b/tests/Makefile.am index 509a07b3..4b05fcaa 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -536,6 +536,9 @@ test_intensive_memory_pool_LDADD = $(COMMON_LIBS) test_tcp_io_SOURCES = test_tcp_io.c test_tcp_io_CFLAGS = -I$(top_srcdir)/lib test_tcp_io_LDADD = $(COMMON_LIBS) +if OS_LINUX +test_tcp_io_LDFLAGS = -Wl,--wrap=uasync_set_timeout -Wl,--wrap=shutdown +endif test_uasync_socket_race_SOURCES = test_uasync_socket_race.c test_uasync_socket_race_CFLAGS = -I$(top_srcdir)/lib diff --git a/tests/test_tcp_io.c b/tests/test_tcp_io.c index 7eb3bfa1..12596a93 100644 --- a/tests/test_tcp_io.c +++ b/tests/test_tcp_io.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "../lib/platform_compat.h" #include "../lib/u_async.h" #include "../lib/tcp_io.h" @@ -16,6 +17,33 @@ #ifndef _WIN32 // POSIX-сценарии не компилируются на Windows, где main уже сообщает SKIP. +#ifdef __linux__ +static int fail_connect_timer; +static int fail_write_shutdown; +void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*); +int __real_shutdown(int, int); + +/* Отказ ровно одной операции: проверяем состояние владельца после ENOMEM/ошибки shutdown. */ +void* __wrap_uasync_set_timeout(struct UASYNC* ua, int timeout_tb, void* arg, timeout_callback_t cb, const char* name) { + if (fail_connect_timer && name && !strcmp(name, "tcp_conn_ct")) { + fail_connect_timer = 0; + DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "test: injected connect timer allocation failure"); + return NULL; + } + return __real_uasync_set_timeout(ua, timeout_tb, arg, cb, name); +} + +int __wrap_shutdown(int fd, int how) { + if (fail_write_shutdown && how == SHUT_WR) { + fail_write_shutdown = 0; + errno = ENOTCONN; + DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "test: injected shutdown failure fd=%d", fd); + return -1; + } + return __real_shutdown(fd, how); +} +#endif + static int tests_run = 0; static int tests_passed = 0; static int tests_failed = 0; @@ -66,6 +94,13 @@ static void reset_counters(void) { g_last_event = 0; } +/* Отменённые записи могут ещё находиться в heap; учитываем только действующие таймеры. */ +static size_t active_timers(struct UASYNC* ua) { + size_t count = 0; + for (size_t i = 0; i < ua->timeout_heap->size; i++) if (!ua->timeout_heap->heap[i].deleted) count++; + return count; +} + static int push_write(struct tcp_conn* tc, const uint8_t* data, size_t len) { size_t offset = 0; while (offset < len) { @@ -273,6 +308,7 @@ static void test_connect_detection(void) { struct tcp_conn* tc = tcp_conn_create(ua, client_fd, 1500, 8192, 32, 8, 0, on_fin, on_error, NULL); ASSERT_TRUE(tc != NULL, "tcp_conn_create failed"); ASSERT_EQ(tc->connected, 0, "should not be connected yet"); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 60000), 0, "arm connect timer"); int ret = connect(client_fd, (struct sockaddr*)&addr, sizeof(addr)); ASSERT_TRUE(ret < 0 && errno == EINPROGRESS, "connect should return EINPROGRESS"); @@ -282,6 +318,10 @@ static void test_connect_detection(void) { uasync_poll(ua, 100); ASSERT_EQ(tc->connected, 1, "connect not detected"); + ASSERT_TRUE(tc->connect_timer == NULL, "successful connect left a timer"); + ASSERT_EQ(active_timers(ua), 0, "successful connect left an active timer"); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 100), 0, "set timeout after connect"); + ASSERT_TRUE(tc->connect_timer == NULL, "connected socket must not arm a connect timer"); close(server_fd); tcp_conn_destroy(tc); @@ -350,7 +390,7 @@ static void test_error_callback(void) { } static void test_connect_timeout(void) { - TEST_START("Connect timeout on black hole (200ms)"); + TEST_START("Connect timer replacement, cancellation, errors and expiry"); #ifdef _WIN32 TEST_PASS(); return; @@ -390,7 +430,36 @@ static void test_connect_timeout(void) { ASSERT_TRUE(tc != NULL, "tcp_conn_create failed"); ASSERT_EQ(tc->connected, 0, "should not be connected"); - tcp_conn_set_connect_timeout(tc, 200); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 10), 0, "first connect timeout"); + void* first = tc->connect_timer; + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 60000), 0, "replace connect timeout"); + void* replacement = tc->connect_timer; + ASSERT_TRUE(replacement && replacement != first, "connect timer not replaced"); + ASSERT_EQ(active_timers(ua), 1, "replacement left multiple active timers"); + uint64_t old_deadline = get_time_tb() + 400; + while (get_time_tb() < old_deadline) uasync_poll(ua, 10); + ASSERT_EQ(g_error_count, 0, "old connect timer fired after replacement"); + ASSERT_TRUE(tc->connect_timer == replacement, "old timer cleared replacement handle"); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, INT_MAX), -EINVAL, "overflowing timeout accepted"); + ASSERT_TRUE(tc->connect_timer == replacement, "invalid timeout changed active timer"); + ASSERT_EQ(tc->connect_timeout_ms, 60000, "invalid timeout changed setting"); +#ifdef __linux__ + fail_connect_timer = 1; + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 200), -ENOMEM, "allocation failure not returned"); + ASSERT_EQ(fail_connect_timer, 0, "allocation failure not injected"); + ASSERT_TRUE(tc->connect_timer == replacement, "allocation failure lost old timer"); + ASSERT_EQ(tc->connect_timeout_ms, 60000, "allocation failure changed timeout"); + ASSERT_EQ(active_timers(ua), 1, "allocation failure left wrong active timer count"); + ASSERT_EQ(g_error_count, 0, "setter called on_error synchronously"); +#endif + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 0), 0, "disable connect timer"); + ASSERT_TRUE(tc->connect_timer == NULL, "disabled timer still has a handle"); + ASSERT_EQ(active_timers(ua), 0, "zero timeout left an active timer"); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, INT_MAX / 10), 0, "maximum valid timeout rejected"); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, -1), 0, "negative timeout must disable timer"); + ASSERT_TRUE(tc->connect_timer == NULL, "negative timeout left a timer"); + ASSERT_EQ(tc->connect_timeout_ms, 0, "negative timeout not normalized"); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 200), 0, "final connect timeout"); uint64_t deadline = get_time_tb() + 10000; while (g_error_count == 0 && get_time_tb() < deadline) uasync_poll(ua, 10); @@ -398,6 +467,8 @@ static void test_connect_timeout(void) { ASSERT_EQ(g_last_error, ETIMEDOUT, "on_error should report ETIMEDOUT"); ASSERT_EQ(tc->connected, 0, "connection should still be not connected"); ASSERT_EQ(tc->error, 1, "tc->error should be set"); + ASSERT_TRUE(tc->connect_timer == NULL, "expired timer handle not cleared before on_error"); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 100), -EINVAL, "errored socket accepted a new timer"); tcp_conn_destroy(tc); for (int i = 0; i < filled; i++) close(fillers[i]); @@ -409,6 +480,39 @@ static void test_connect_timeout(void) { #endif } +/* После отложенного free прокручиваем event loop за сроки всех отменённых таймеров. */ +static void test_connect_timer_destroy(void) { + TEST_START("Destroy after rearming or disabling connect timer"); + for (int disable = 0; disable < 2; disable++) { + size_t allocated = u_get_allocated_count(); + uasync_t* ua = uasync_create(); + ASSERT_TRUE(ua != NULL, "uasync_create failed"); + reset_counters(); + int fd = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_TRUE(fd >= 0, "socket failed"); + ASSERT_EQ(socket_set_nonblocking(fd), 0, "nonblocking failed"); + struct tcp_conn* tc = tcp_conn_create(ua, fd, 1500, 8192, 32, 8, 0, on_fin, on_error, NULL); + ASSERT_TRUE(tc != NULL, "tcp_conn_create failed"); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 1), 0, "first timeout"); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 2), 0, "replacement timeout"); + if (disable) ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 0), 0, "disable timeout"); + ASSERT_EQ(tcp_conn_push_fin(tc), 0, "queue FIN before destroy"); + ASSERT_EQ(tc->fin_pending, 1, "unconnected socket must keep FIN pending"); + tcp_conn_destroy(tc); + ASSERT_TRUE(tc->connect_timer == NULL, "destroy left timer handle"); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 1), -EINVAL, "destroyed socket accepted a new timer"); + uasync_drain_immediate(ua); + uint64_t deadline = get_time_tb() + 100; + while (get_time_tb() < deadline) uasync_poll(ua, 10); + ASSERT_EQ(g_error_count, 0, "cancelled timer called on_error after destroy"); + ASSERT_EQ(active_timers(ua), 0, "destroy left active timers"); + ASSERT_EQ(ua->timer_alloc_count, ua->timer_free_count, "timer allocations leaked"); + uasync_destroy(ua, 0); + ASSERT_EQ(u_get_allocated_count(), allocated, "destroy leaked memory"); + } + TEST_PASS(); +} + // ==================================================================== // FIN / Close через очередь — новые тесты // ==================================================================== @@ -552,7 +656,7 @@ static void test_fin_data_ordering(void) { } static void test_double_push_fin(void) { - TEST_START("Double push_fin ignored"); + TEST_START("Double push_fin rejected while pending and after shutdown"); uasync_t* ua = uasync_create(); ASSERT_TRUE(ua != NULL, "uasync_create failed"); @@ -566,11 +670,18 @@ static void test_double_push_fin(void) { ASSERT_TRUE(tc != NULL, "tcp_conn_create failed"); tc->on_fin_sent = on_fin_sent_cb; + queue_set_callback_defer(tc->write_queue, 1); ASSERT_EQ(tcp_conn_push_fin(tc), 0, "first push_fin"); + ASSERT_EQ(tc->fin_pending, 1, "FIN should still be queued"); + ASSERT_EQ(tc->fin_local, 0, "FIN marked sent before shutdown"); ASSERT_EQ(tcp_conn_push_fin(tc), -1, "second push_fin should return -1"); + ASSERT_EQ(tc->write_queue->count, 1, "duplicate FIN entered queue"); uasync_poll(ua, 10); ASSERT_EQ(g_fin_sent_count, 1, "on_fin_sent called more than once"); + ASSERT_EQ(tc->fin_pending, 0, "FIN pending flag not cleared after shutdown"); + ASSERT_EQ(tc->fin_local, 1, "successful shutdown not recorded"); + ASSERT_EQ(tcp_conn_push_fin(tc), -1, "FIN accepted after shutdown"); tcp_conn_destroy(tc); close(sv[1]); @@ -580,6 +691,35 @@ static void test_double_push_fin(void) { TEST_PASS(); } +#ifdef __linux__ +static void test_fin_shutdown_error(void) { + TEST_START("Failed FIN shutdown does not report on_fin_sent"); + uasync_t* ua = uasync_create(); + ASSERT_TRUE(ua != NULL, "uasync_create failed"); + reset_counters(); + int sv[2]; + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0, "socketpair failed"); + for (int i = 0; i < 2; i++) ASSERT_EQ(socket_set_nonblocking(sv[i]), 0, "nonblocking failed"); + struct tcp_conn* tc = tcp_conn_create(ua, sv[0], 1500, 8192, 32, 8, 0, on_fin, on_error, NULL); + ASSERT_TRUE(tc != NULL, "tcp_conn_create failed"); + tc->on_fin_sent = on_fin_sent_cb; + fail_write_shutdown = 1; + ASSERT_EQ(tcp_conn_push_fin(tc), 0, "FIN sentinel not accepted"); + ASSERT_EQ(fail_write_shutdown, 0, "shutdown failure not injected"); + ASSERT_EQ(g_fin_sent_count, 0, "failed shutdown reported FIN sent"); + ASSERT_EQ(tc->fin_local, 0, "failed shutdown set fin_local"); + ASSERT_EQ(tc->fin_pending, 0, "failed shutdown left FIN pending"); + ASSERT_EQ(g_error_count, 1, "failed shutdown did not call on_error"); + ASSERT_EQ(g_last_error, ENOTCONN, "wrong shutdown error"); + tcp_conn_destroy(tc); + close(sv[1]); + uasync_drain_immediate(ua); + ASSERT_EQ(ua->timer_alloc_count, ua->timer_free_count, "timer allocations leaked"); + uasync_destroy(ua, 0); + TEST_PASS(); +} +#endif + static void test_double_push_close(void) { TEST_START("Double push_close ignored"); @@ -673,10 +813,14 @@ int main(void) { test_connect_detection(); test_error_callback(); test_connect_timeout(); + test_connect_timer_destroy(); test_push_fin(); test_push_close(); test_fin_data_ordering(); test_double_push_fin(); +#ifdef __linux__ + test_fin_shutdown_error(); +#endif test_double_push_close(); test_fin_before_close();