diff --git a/tests/test_radio.c b/tests/test_radio.c index 4ab201fa..0402927d 100644 --- a/tests/test_radio.c +++ b/tests/test_radio.c @@ -13,6 +13,7 @@ #include "chat_core.h" #include "chat_sync.h" #include "chat_event.h" +#include "chat_member.h" #include "member_sync.h" #include "../radio/radio.h" #include "../routing_layer/topo_node_sqlite.h" @@ -59,6 +60,9 @@ enum rad_phase { P_WAIT_OFF_C, P_TALK2, P_WAIT_RX2, + P_REENABLE_C, + P_WAIT_REENABLE_C, + P_WITHDRAW_C, P_DONE, }; @@ -89,6 +93,63 @@ struct rad_shared { }; static struct rad_shared g_sh; static struct rx_evt g_rx[3]; +static int g_member_radio[3][3]; +static int g_member_lists[3]; +static int g_member_event_bad; + +/* Проверяем именно UI-события: self, прямой пир и пир за посредником. */ +static void member_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + if (type != CHAT_EVT_MEMBER_UPDATED && type != CHAT_EVT_MEMBER_LIST) return; + if (!data || len < 3) { g_member_event_bad = 1; return; } + int cl = data[0]; + if (cl != (int)strlen(CH_ID) || len < cl + 3 || memcmp(data + 1, CH_ID, cl)) return; + int count = data[cl + 1]; + int off = cl + 2; + if (type == CHAT_EVT_MEMBER_LIST) { count |= data[cl + 2] << 8; off++; } + if (len != off + count * CHAT_MEMBER_DISPLAY_SIZE) { g_member_event_bad = 1; return; } + int dst = -1; + for (int i = 0; i < 3; i++) if (inst->node_id == g_sh.nid[i]) dst = i; + if (dst < 0) { g_member_event_bad = 1; return; } + if (type == CHAT_EVT_MEMBER_LIST) g_member_lists[dst]++; + for (int i = 0; i < count; i++) { + struct chat_member_display member; + if (chat_core_deserialize_member(data + off + i * CHAT_MEMBER_DISPLAY_SIZE, &member) < 0) { + g_member_event_bad = 1; return; + } + for (int j = 0; j < 3; j++) + if (member.node_id == g_sh.nid[j]) g_member_radio[dst][j] = !!(member.flags & CHAT_MEMBER_FLAG_RADIO); + } +} + +/* Полный список, одиночная строка и текущие события должны совпадать. */ +static int check_member_radios(struct UTUN_INSTANCE* inst, int dst, int c_radio) { + uint8_t* wire = NULL; int count = 0; + if (chat_core_get_member_list(inst, CH_ID, &wire, &count) < 0 || count != 3 || g_member_event_bad) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: invalid member list dst=%d count=%d event_bad=%d", dst, count, g_member_event_bad); + u_free(wire); return 0; + } + int seen = 0; + for (int i = 0; i < count; i++) { + struct chat_member_display member; + chat_core_deserialize_member(wire + i * CHAT_MEMBER_DISPLAY_SIZE, &member); + for (int j = 0; j < 3; j++) { + if (member.node_id != g_sh.nid[j]) continue; + uint8_t single[CHAT_MEMBER_DISPLAY_SIZE]; + int expected = j == IDX_C ? c_radio : 1; + int radio = !!(member.flags & CHAT_MEMBER_FLAG_RADIO); + if (radio != expected || g_member_radio[dst][j] != expected + || chat_core_get_single_member(inst, CH_ID, member.node_id, single) < 0 + || !!(single[10] & CHAT_MEMBER_FLAG_RADIO) != expected) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: member radio dst=%d src=%d list=%d event=%d expected=%d", + dst, j, radio, g_member_radio[dst][j], expected); + u_free(wire); return 0; + } + seen |= 1 << j; + } + } + u_free(wire); + return seen == 7; +} static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0; #define TEST(n) do { G_TOTAL++; printf(" %-58s", n); fflush(stdout); } while(0) @@ -325,6 +386,14 @@ static void rad_tick(void* arg) { t->phase = P_TALK1; break; case P_TALK1: + for (int i = 0; i < 3; i++) { + if (!check_member_radios(t->inst[i], i, 1)) { t->result = 2; uasync_stop(t->ua); return; } + struct chat_member_list_req* req = u_calloc(1, sizeof(*req)); + if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: member request OOM"); t->result = 2; uasync_stop(t->ua); return; } + req->inst = t->inst[i]; strcpy(req->ch_id, CH_ID); + chat_core_request_member_list_trampoline(req); + if (g_member_lists[i] != 1) { t->result = 2; uasync_stop(t->ua); return; } + } memset(g_rx, 0, sizeof(g_rx)); do_talk(A); t->phase = P_WAIT_RX1; @@ -346,14 +415,40 @@ static void rad_tick(void* arg) { } break; case P_TALK2: + for (int i = 0; i < 3; i++) + if (!check_member_radios(t->inst[i], i, 0)) { t->result = 2; uasync_stop(t->ua); return; } do_talk(A); t->phase = P_WAIT_RX2; break; case P_WAIT_RX2: if (g_rx[IDX_B].frames == N_FRAMES && g_rx[IDX_B].fin_seen && g_rx[IDX_B].seq_ok && g_rx[IDX_C].frames == 0 && !g_rx[IDX_C].fin_seen) - t->phase = P_DONE; + t->phase = P_REENABLE_C; + break; + case P_REENABLE_C: + radio_set_active(C, gid, 1); + t->phase = P_WAIT_REENABLE_C; + break; + case P_WAIT_REENABLE_C: + if (node_radio(A, nc) && node_radio(B, nc)) t->phase = P_WITHDRAW_C; break; + case P_WITHDRAW_C: { + struct TOPO_GROUP* group = topo_groups_find(A->topo_groups, gid); + struct TOPO_GROUP_NODE* node = topo_node_find_by_id(group, nc); + while (node && node->paths) { + struct ETCP_CONN* from = ((struct TOPO_NODEPATH*)node->paths->head)->conn; + struct TOPOMSG_WITHDRAW_PKT withdraw = { .node_id = nc }; + if (topo_group_process_withdraw(group, from, (const uint8_t*)&withdraw, sizeof(withdraw)) < 0) { + t->result = 2; uasync_stop(t->ua); return; + } + node = topo_node_find_by_id(group, nc); + } + if (!radio_is_active(C, gid) || !check_member_radios(A, IDX_A, 0)) { + t->result = 2; uasync_stop(t->ua); return; + } + t->phase = P_DONE; + break; + } case P_DONE: t->result = 1; uasync_stop(t->ua); @@ -397,7 +492,7 @@ int main(int argc, char** argv) { printf("=== test_radio ===\n"); fflush(stdout); - TEST("single-thread: radio flag + talk (seq/fin) + relay + prune (3 nodes)"); { + TEST("radio: member snapshots/events + relay + off + withdraw"); { struct UASYNC* ua = uasync_create(); if (!ua) { FAIL("uasync_create failed"); return 1; } @@ -422,6 +517,8 @@ int main(int argc, char** argv) { utun_instance_init(t.inst[IDX_A]); utun_instance_init(t.inst[IDX_B]); utun_instance_init(t.inst[IDX_C]); + memset(g_member_radio, -1, sizeof(g_member_radio)); + for (int i = 0; i < 3; i++) chat_event_set_handler(t.inst[i], member_event); radio_set_frame_cb(t.inst[IDX_A], rx_frame_cb, &g_rx[IDX_A]); radio_set_frame_cb(t.inst[IDX_B], rx_frame_cb, &g_rx[IDX_B]);