// test_radio.c — интеграционный тест рации (PTT мультикаст голоса). // // Однопоточный: 3 узла (A, B, C) в одном процессе, один UASYNC, без fork. // Топология — цепочка A←B←C (A сервер, B клиент к A + сервер, C клиент к B). // Все три в одной CHAT-группе. Master state-machine на таймере. // // Сценарий: // 1. A,B,C radio_on → флаг TOPO_FLAG_RADIO распространяется по BGP (node.radio==1). // 2. A: radio_talk_begin + 5 кадров + radio_talk_end → B и C (через relay B) получают // кадры в порядке seq 0..4 и FIN. // 3. C radio_off (обрезание) → A снова говорит → B получает, C не получает. // 4. Без FIN: C снимает индикацию при radio_off, B — по таймауту источника. #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" #include "../routing_layer/topo_group.h" #include "../routing_layer/topo_node.h" #include "../utun_instance.h" #include "../transport_layer/etcp.h" #include "../transport_layer/secure_channel.h" #include "../ntp_time.h" #include "../lib/u_async.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "../lib/platform_compat.h" #include "test_utils.h" #include #define OPENSSL_API_COMPAT 0x10100000L #include #include #include #include #include #include #define CH_ID "4242424242424242" #define IDX_A 0 #define IDX_B 1 #define IDX_C 2 #define TICK_TB 100 /* 10 ms на такт */ #define MAX_TICKS 4000 /* глобальный таймаут ≈ 40 c */ #define N_FRAMES 5 enum rad_phase { P_WAIT_CONN, P_SETUP, P_WAIT_BGP, P_RADIO_ON, P_WAIT_FLAGS, P_TALK1, P_WAIT_RX1, P_WAIT_MUTED_RELAY, P_RADIO_OFF_C, P_WAIT_OFF_C, P_TALK2, P_WAIT_RX2, P_REENABLE_C, P_WAIT_REENABLE_C, P_TALK_STALLED, P_WAIT_STALLED_START, P_WAIT_STALLED_STOP, P_WAIT_RESTORED_C, P_WITHDRAW_C, P_DONE, }; struct rad_ctx { struct UASYNC* ua; struct UTUN_INSTANCE* inst[3]; enum rad_phase phase; int result; /* 0=running, 1=ok, 2=fail */ int ticks; void* timer; }; /* приёмник кадров (per-узел): проверка порядка seq и FIN */ struct rx_evt { int active; uint16_t stream; uint16_t expect_seq; int frames; /* число data-кадров (fin=0) текущего потока */ int fin_seen; int seq_ok; }; struct rad_shared { uint8_t ch_x_pub[32], ch_ed_pub[32], ch_ed_priv[32], ch_sig[64]; uint64_t nid[3]; uint8_t x_pub[3][32], x_priv[3][32], ed_pub[3][32]; int port[3]; }; 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; static int g_talk_on[3], g_talk_starts[3], g_talk_stops[3]; /* Проверяем именно UI-события: self, прямой пир и пир за посредником. */ static void member_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { if (type == CHAT_EVT_RADIO_TALK) { uint64_t group, source; if (!data || len != 19 || data[18] > 1) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: invalid talk event bytes=%d", len); g_member_event_bad = 1; return; } memcpy(&group, data, 8); memcpy(&source, data + 8, 8); int dst = -1; for (int i = 0; i < 3; ++i) if (inst->node_id == g_sh.nid[i]) dst = i; if (dst < 0 || group != strtoull(CH_ID, NULL, 10) || source != g_sh.nid[IDX_A]) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: unexpected talk dst=%d group=%016llx source=%016llx", dst, (unsigned long long)group, (unsigned long long)source); g_member_event_bad = 1; return; } g_talk_on[dst] = data[18]; if (data[18]) ++g_talk_starts[dst]; else ++g_talk_stops[dst]; DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "test: talk event dst=%d on=%d starts=%d stops=%d", dst, data[18], g_talk_starts[dst], g_talk_stops[dst]); return; } 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) #define OK() do { G_PASSED++; printf("OK\n"); } while(0) #define FAIL(f,...) do { G_FAILED++; printf("FAIL: " f "\n", ##__VA_ARGS__); } while(0) /* ── крипто-хелперы ── */ static void gen_x25519(uint8_t pub[32], uint8_t priv[32]) { EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); EVP_PKEY* pkey = NULL; EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx); size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l); l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l); EVP_PKEY_free(pkey); } static void gen_ed25519(uint8_t pub[32], uint8_t priv[32]) { EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); EVP_PKEY* pkey = NULL; EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx); size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l); l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l); EVP_PKEY_free(pkey); } static void to_hex(const uint8_t* bin, size_t n, char* out) { for (size_t i = 0; i < n; i++) sprintf(out + i * 2, "%02x", bin[i]); out[n * 2] = '\0'; } static int wf(const char* p, const char* f, ...) { va_list ap; FILE* fp = fopen(p, "w"); if (!fp) return -1; va_start(ap, f); vfprintf(fp, f, ap); va_end(ap); fclose(fp); return 0; } /* ── данные сценария + конфиги (цепочка A←B←C) ── */ static void fill_shared(int base_port) { memset(&g_sh, 0, sizeof(g_sh)); for (int i = 0; i < 3; i++) { gen_x25519(g_sh.x_pub[i], g_sh.x_priv[i]); sc_derive_ed25519_pubkey(g_sh.x_priv[i], g_sh.ed_pub[i]); g_sh.nid[i] = sc_derive_node_id_from_pubkey(g_sh.x_pub[i]); g_sh.port[i] = base_port + i * 1000; } { uint8_t ch_x_priv[32]; gen_x25519(g_sh.ch_x_pub, ch_x_priv); } gen_ed25519(g_sh.ch_ed_pub, g_sh.ch_ed_priv); { uint8_t msg[256]; size_t off = 0; off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", CH_ID) + 1; off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", "radio-test") + 1; memcpy(msg + off, &g_sh.nid[IDX_A], 8); off += 8; memcpy(msg + off, g_sh.ch_x_pub, 32); off += 32; memcpy(msg + off, g_sh.ch_ed_pub, 32); off += 32; sc_ed25519_sign(g_sh.ch_ed_priv, msg, off, g_sh.ch_sig); } } static void write_configs(const char* dir) { for (int i = 0; i < 3; i++) { char priv[65], pub[65], path[512], dbd[512]; to_hex(g_sh.x_priv[i], 32, priv); to_hex(g_sh.x_pub[i], 32, pub); snprintf(path, sizeof(path), "%s/%s.conf", dir, i == IDX_A ? "a" : i == IDX_B ? "b" : "c"); snprintf(dbd, sizeof(dbd), "%s/db%s", dir, i == IDX_A ? "a" : i == IDX_B ? "b" : "c"); utun_mkdir(dbd, 0755); char client[512] = ""; if (i == IDX_B) { /* B → A */ char apub[65]; to_hex(g_sh.x_pub[IDX_A], 32, apub); snprintf(client, sizeof(client), "[client: to_a]\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n", apub, g_sh.port[IDX_A]); } else if (i == IDX_C) { /* C → B */ char bpub[65]; to_hex(g_sh.x_pub[IDX_B], 32, bpub); snprintf(client, sizeof(client), "[client: to_b]\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n", bpub, g_sh.port[IDX_B]); } wf(path, "[global]\ntun_ip=10.98.%d.1/24\ntun_ifname=tun%d0\ndb_path=%s/db%s\n" "my_private_key=%s\nmy_public_key=%s\n" "[server: s1]\naddr=127.0.0.1:%d\ntype=public\n" "%s" "[chatserver]\nstorage_autoload=0\n[allowed_keys]\nallow_all=1\n", i, i, dir, i == IDX_A ? "a" : i == IDX_B ? "b" : "c", priv, pub, g_sh.port[i], client); } } /* ── проверки ── */ static int conn_up(struct UTUN_INSTANCE* inst, uint64_t nid) { struct ETCP_CONN* c = instance_find_conn(inst, nid); return c && c->initialized && c->links_up; } static int bgp_has(struct UTUN_INSTANCE* inst, uint64_t nid) { uint64_t gid = strtoull(CH_ID, NULL, 10); struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid); if (!g) return 0; struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, nid); return nq && nq->paths && nq->paths->head; } static int node_radio(struct UTUN_INSTANCE* inst, uint64_t nid) { uint64_t gid = strtoull(CH_ID, NULL, 10); struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid); if (!g) return 0; struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, nid); return nq ? nq->radio : 0; } /* ── frame cb (per-узел) ── */ static void rx_frame_cb(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t src, uint16_t stream, uint16_t seq, uint8_t fin, const uint8_t* opus, int len, void* arg) { (void)opus; (void)len; if (radio_member_muted(inst, group_id, src)) return; /* локальный аудио-потребитель */ struct rx_evt* rx = (struct rx_evt*)arg; if (fin) { if (rx->active && stream == rx->stream) rx->fin_seen = 1; return; } if (!rx->active || stream != rx->stream) { rx->active = 1; rx->stream = stream; rx->expect_seq = 0; rx->frames = 0; rx->fin_seen = 0; rx->seq_ok = 1; } if (seq != rx->expect_seq) rx->seq_ok = 0; rx->expect_seq = (uint16_t)(seq + 1); rx->frames++; } /* ── настройка канала и мемберов ── */ static void channel_put_shared(struct UTUN_INSTANCE* inst, int has_priv) { topo_node_sqlite_channel_put(inst->topo_sqlite_db, CH_ID, "radio-test", g_sh.nid[IDX_A], g_sh.ch_x_pub, NULL, g_sh.ch_ed_pub, has_priv ? g_sh.ch_ed_priv : NULL, g_sh.ch_sig); chat_core_ensure_channel_ready(inst, CH_ID); } static void member_put_self(struct UTUN_INSTANCE* inst, const char* name) { uint64_t join_ts = (uint64_t)ntp_time_get_seconds(inst); uint8_t jm[128]; int jl = member_sync_build_join_msg(g_sh.ch_x_pub, g_sh.ch_ed_pub, inst->node_id, inst->my_keys.public_key, join_ts, jm, (int)sizeof(jm)); uint8_t join_sig[64]; sc_ed25519_sign(inst->my_ed25519_privkey, jm, (size_t)jl, join_sig); char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name); member_sync_put(inst, CH_ID, inst->node_id, inst->my_keys.public_key, inst->my_ed25519_pubkey, join_sig, join_ts, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL); } static void member_put_placeholder(struct UTUN_INSTANCE* inst, int idx, const char* name) { char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name); member_sync_put(inst, CH_ID, g_sh.nid[idx], g_sh.x_pub[idx], g_sh.ed_pub[idx], NULL, 0, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL); } static void do_setup(struct UTUN_INSTANCE* inst, int role) { switch (role) { case IDX_A: channel_put_shared(inst, 1); member_put_self(inst, "A"); member_put_placeholder(inst, IDX_B, "B"); member_put_placeholder(inst, IDX_C, "C"); member_sync_start(inst, g_sh.nid[IDX_B], CH_ID, NULL, NULL); break; case IDX_B: channel_put_shared(inst, 0); member_put_self(inst, "B"); member_put_placeholder(inst, IDX_A, "A"); member_put_placeholder(inst, IDX_C, "C"); member_sync_start(inst, g_sh.nid[IDX_A], CH_ID, NULL, NULL); member_sync_start(inst, g_sh.nid[IDX_C], CH_ID, NULL, NULL); break; case IDX_C: channel_put_shared(inst, 0); member_put_self(inst, "C"); member_put_placeholder(inst, IDX_A, "A"); member_put_placeholder(inst, IDX_B, "B"); member_sync_start(inst, g_sh.nid[IDX_B], CH_ID, NULL, NULL); break; } } /* ── talk: 5 кадров + FIN от A ── */ static void do_talk(struct UTUN_INSTANCE* A) { uint64_t gid = strtoull(CH_ID, NULL, 10); static const uint8_t opus[16] = { 0xf8, 0xff, 0xfe, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d }; radio_talk_begin(A, gid); for (int i = 0; i < N_FRAMES; i++) radio_talk_send(A, gid, opus, sizeof(opus)); radio_talk_end(A, gid); } /* ── master state-machine ── */ static void rad_tick(void* arg) { struct rad_ctx* t = arg; t->timer = NULL; if (t->result) return; struct UTUN_INSTANCE* A = t->inst[IDX_A]; struct UTUN_INSTANCE* B = t->inst[IDX_B]; struct UTUN_INSTANCE* C = t->inst[IDX_C]; uint64_t na = g_sh.nid[IDX_A], nb = g_sh.nid[IDX_B], nc = g_sh.nid[IDX_C]; uint64_t gid = strtoull(CH_ID, NULL, 10); switch (t->phase) { case P_WAIT_CONN: if (conn_up(A, nb) && conn_up(B, na) && conn_up(B, nc) && conn_up(C, nb)) t->phase = P_SETUP; break; case P_SETUP: do_setup(A, IDX_A); do_setup(B, IDX_B); do_setup(C, IDX_C); t->phase = P_WAIT_BGP; break; case P_WAIT_BGP: if (bgp_has(A, nb) && bgp_has(A, nc) && bgp_has(B, na) && bgp_has(B, nc) && bgp_has(C, na) && bgp_has(C, nb)) t->phase = P_RADIO_ON; break; case P_RADIO_ON: radio_set_active(A, gid, 1); radio_set_active(B, gid, 1); radio_set_active(C, gid, 1); t->phase = P_WAIT_FLAGS; break; case P_WAIT_FLAGS: if (node_radio(A, nb) && node_radio(A, nc) && node_radio(B, na) && node_radio(B, nc) && node_radio(C, na) && node_radio(C, nb)) 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; break; case P_WAIT_RX1: 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 == N_FRAMES && g_rx[IDX_C].fin_seen && g_rx[IDX_C].seq_ok) { if (chat_core_toggle_member_radio_mute(B, CH_ID, na) < 0 || !radio_member_muted(B, gid, na) || radio_member_muted(C, gid, na)) { t->result = 2; uasync_stop(t->ua); return; } memset(g_rx, 0, sizeof(g_rx)); do_talk(A); t->phase = P_WAIT_MUTED_RELAY; } break; case P_WAIT_MUTED_RELAY: if (g_rx[IDX_C].frames == N_FRAMES && g_rx[IDX_C].fin_seen && g_rx[IDX_C].seq_ok) { if (g_rx[IDX_B].frames || g_rx[IDX_B].fin_seen || !radio_is_active(B, gid) || chat_core_toggle_member_radio_mute(B, CH_ID, na) < 0 || radio_member_muted(B, gid, na)) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: locally muted relay blocked downstream or local mute failed"); t->result = 2; uasync_stop(t->ua); return; } t->phase = P_RADIO_OFF_C; } break; case P_RADIO_OFF_C: radio_set_active(C, gid, 0); t->phase = P_WAIT_OFF_C; break; case P_WAIT_OFF_C: /* ждём, пока A и B увидят radio=0 у C (обрезание ветви C) */ if (!node_radio(A, nc) && !node_radio(B, nc)) { memset(g_rx, 0, sizeof(g_rx)); t->phase = P_TALK2; } 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_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_TALK_STALLED; break; case P_TALK_STALLED: { const uint8_t opus[] = {0xf8, 0xff, 0xfe}; memset(g_rx, 0, sizeof(g_rx)); if (radio_talk_begin(A, gid) < 0 || radio_talk_send(A, gid, opus, sizeof(opus)) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: cannot start burst without FIN"); t->result = 2; uasync_stop(t->ua); return; } t->phase = P_WAIT_STALLED_START; break; } case P_WAIT_STALLED_START: if (g_rx[IDX_B].frames && g_rx[IDX_C].frames) { if (!g_talk_on[IDX_B] || !g_talk_on[IDX_C] || g_member_event_bad) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: missing talk START B=%d C=%d bad=%d", g_talk_on[IDX_B], g_talk_on[IDX_C], g_member_event_bad); t->result = 2; uasync_stop(t->ua); return; } radio_set_active(C, gid, 0); if (g_talk_on[IDX_C] || !g_talk_on[IDX_B]) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: subscription stop did not clear only C: B=%d C=%d", g_talk_on[IDX_B], g_talk_on[IDX_C]); t->result = 2; uasync_stop(t->ua); return; } t->phase = P_WAIT_STALLED_STOP; } break; case P_WAIT_STALLED_STOP: if (!g_talk_on[IDX_B]) { if (g_rx[IDX_B].fin_seen || g_rx[IDX_C].fin_seen || g_member_event_bad) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: stopped indication without FIN check failed B_fin=%d C_fin=%d bad=%d", g_rx[IDX_B].fin_seen, g_rx[IDX_C].fin_seen, g_member_event_bad); t->result = 2; uasync_stop(t->ua); return; } DEBUG_INFO(DEBUG_CATEGORY_RADIO, "test: talk STOP without FIN verified (C unsubscribe, B timeout)"); radio_talk_end(A, gid); radio_set_active(C, gid, 1); t->phase = P_WAIT_RESTORED_C; } break; case P_WAIT_RESTORED_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: if (g_member_event_bad || g_talk_starts[IDX_B] != 4 || g_talk_stops[IDX_B] != 4 || g_talk_starts[IDX_C] != 3 || g_talk_stops[IDX_C] != 3 || g_talk_on[IDX_B] || g_talk_on[IDX_C]) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test: talk lifecycle mismatch B=%d/%d C=%d/%d on=%d/%d bad=%d", g_talk_starts[IDX_B], g_talk_stops[IDX_B], g_talk_starts[IDX_C], g_talk_stops[IDX_C], g_talk_on[IDX_B], g_talk_on[IDX_C], g_member_event_bad); t->result = 2; uasync_stop(t->ua); return; } t->result = 1; uasync_stop(t->ua); return; } if (++t->ticks > MAX_TICKS) { fprintf(stderr, "test: timeout in phase %d (B_rx=%d/%d fin=%d seqok=%d C_rx=%d/%d fin=%d seqok=%d radio_flags: A(B=%d C=%d) B(A=%d C=%d) C(A=%d B=%d))\n", (int)t->phase, g_rx[IDX_B].frames, N_FRAMES, g_rx[IDX_B].fin_seen, g_rx[IDX_B].seq_ok, g_rx[IDX_C].frames, N_FRAMES, g_rx[IDX_C].fin_seen, g_rx[IDX_C].seq_ok, node_radio(A, nb), node_radio(A, nc), node_radio(B, na), node_radio(B, nc), node_radio(C, na), node_radio(C, nb)); t->result = 2; uasync_stop(t->ua); return; } t->timer = uasync_set_timeout(t->ua, TICK_TB, t, rad_tick, "rad_tick"); } /* ── main ── */ int main(int argc, char** argv) { (void)argc; (void)argv; debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); if (getenv("UTUN_TEST_DEBUG")) { debug_set_category_level_by_name("radio", "debug"); debug_set_category_level_by_name("bgp", "info"); debug_set_category_level_by_name("member_sync", "info"); debug_set_category_level_by_name("chat_sync", "info"); } utun_instance_set_tun_init_enabled(0); srand((unsigned)time(NULL)); char dir[512]; snprintf(dir, sizeof(dir), "/tmp/utun_radio_XXXXXX"); if (test_mkdtemp(dir) != 0) { fprintf(stderr, "mkdtemp failed\n"); return 1; } test_setenv("UTUN_TEST_DIR", dir, 1); fill_shared(58000 + (getpid() % 2000)); write_configs(dir); printf("=== test_radio ===\n"); fflush(stdout); TEST("radio: members + mute relay + speaker FIN/timeout/off + withdraw"); { struct UASYNC* ua = uasync_create(); if (!ua) { FAIL("uasync_create failed"); return 1; } struct rad_ctx t; memset(&t, 0, sizeof(t)); t.ua = ua; t.phase = P_WAIT_CONN; char cfgA[512], cfgB[512], cfgC[512]; snprintf(cfgA, sizeof(cfgA), "%s/a.conf", dir); snprintf(cfgB, sizeof(cfgB), "%s/b.conf", dir); snprintf(cfgC, sizeof(cfgC), "%s/c.conf", dir); t.inst[IDX_A] = utun_instance_create(ua, cfgA); t.inst[IDX_B] = utun_instance_create(ua, cfgB); t.inst[IDX_C] = utun_instance_create(ua, cfgC); if (!t.inst[IDX_A] || !t.inst[IDX_B] || !t.inst[IDX_C]) { FAIL("instance create failed A=%p B=%p C=%p", (void*)t.inst[IDX_A], (void*)t.inst[IDX_B], (void*)t.inst[IDX_C]); uasync_destroy(ua, 0); return 1; } 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]); radio_set_frame_cb(t.inst[IDX_C], rx_frame_cb, &g_rx[IDX_C]); t.timer = uasync_set_timeout(ua, TICK_TB, &t, rad_tick, "rad_tick"); uasync_mainloop(ua); int ok = (t.result == 1); if (t.inst[IDX_A]) utun_instance_destroy(t.inst[IDX_A]); if (t.inst[IDX_B]) utun_instance_destroy(t.inst[IDX_B]); if (t.inst[IDX_C]) utun_instance_destroy(t.inst[IDX_C]); uasync_destroy(ua, 0); if (ok) OK(); else FAIL("result=%d phase=%d", t.result, (int)t.phase); } char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", dir); (void)!system(cmd); printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED); return G_FAILED > 0 ? 1 : 0; }