diff --git a/tests/test_radio.c b/tests/test_radio.c index d8c43dbc..0b21f0b0 100644 --- a/tests/test_radio.c +++ b/tests/test_radio.c @@ -17,6 +17,8 @@ #include "chat_member.h" #include "member_sync.h" #include "../radio/radio.h" +#include "../radio/radio_call.h" +#include "../routing_layer/etcp_router.h" #include "../routing_layer/topo_node_sqlite.h" #include "../routing_layer/topo_group.h" #include "../routing_layer/topo_node.h" @@ -48,13 +50,14 @@ #define IDX_C 2 #define TICK_TB 100 /* 10 ms на такт */ -#define MAX_TICKS 4000 /* глобальный таймаут ≈ 40 c */ +#define MAX_TICKS 12000 /* включает минуту ожидания решения по вызову */ #define N_FRAMES 5 enum rad_phase { P_WAIT_CONN, P_SETUP, P_WAIT_BGP, + P_RADIO_CALLS, P_RADIO_ON, P_WAIT_FLAGS, P_TALK1, @@ -81,6 +84,8 @@ struct rad_ctx { int result; /* 0=running, 1=ok, 2=fail */ int ticks; void* timer; + int call_step; + uint64_t call_started; }; /* приёмник кадров (per-узел): проверка порядка seq и FIN */ @@ -104,9 +109,48 @@ 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]; +static struct { int state, reason, finished, incoming; uint64_t finished_tb; } g_calls[6][3]; +static int g_call_ready, g_drop_result, g_drop_ack; +static etcp_recv_fn g_call_recv[3]; + +/* Потеря приложения после router ACK: восстановить результат обязан сервис, не транспорт. */ +static void call_packet_loss(struct ETCP_CONN* conn, struct ll_entry* entry) { + int idx = -1; + for (int i = 0; i < 3; ++i) if (conn->instance->node_id == g_sh.nid[i]) idx = i; + if (idx < 0 || !entry || entry->len <= ROUTER_SVC_PAYLOAD_OFF) abort(); + int cmd = entry->dgram[ROUTER_SVC_PAYLOAD_OFF]; + if ((idx == IDX_A && cmd == 3 && g_drop_result-- > 0) || (idx == IDX_C && cmd == 4 && g_drop_ack-- > 0)) { + DEBUG_INFO(DEBUG_CATEGORY_RADIO, "test drops radio result/ACK dst=%d command=%d", idx, cmd); + queue_dgram_free(entry); queue_entry_free(entry); return; + } + g_call_recv[idx](conn, entry); +} + +static void radio_action(struct UTUN_INSTANCE* inst, uint64_t id, int action, uint64_t peer) { + struct radio_call_request req = {inst, id, strtoull(CH_ID, NULL, 10), peer, action}; + radio_call_execute(&req); +} /* Проверяем именно UI-события: self, прямой пир и пир за посредником. */ static void member_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + if (type == CHAT_EVT_RADIO_CALL) { + uint64_t id, group, peer; + if (!data || len != 27) { g_member_event_bad = 1; return; } + memcpy(&id, data, 8); memcpy(&group, data + 8, 8); memcpy(&peer, data + 16, 8); + int idx = -1; + for (int i = 0; i < 3; ++i) if (inst->node_id == g_sh.nid[i]) idx = i; + if (idx < 0 || id < 100 || id > 105 || group != strtoull(CH_ID, NULL, 10)) { g_member_event_bad = 1; return; } + g_calls[id - 100][idx].state = data[24]; g_calls[id - 100][idx].reason = data[25]; + if (data[24] == RADIO_CALL_INCOMING) { + ++g_calls[id - 100][idx].incoming; + if (g_call_ready) radio_action(inst, id, RADIO_CALL_READY, 0); + } + if (data[24] == RADIO_CALL_FINISHED) { + ++g_calls[id - 100][idx].finished; g_calls[id - 100][idx].finished_tb = get_time_tb(); + if (data[25] == RADIO_CALL_ACCEPTED) radio_set_active(inst, group, 1); + } + return; + } if (type == CHAT_EVT_RADIO_TALK) { uint64_t group, source; if (!data || len != 19 || data[18] > 1) { @@ -327,9 +371,10 @@ static void member_put_self(struct UTUN_INSTANCE* inst, const char* name) { 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); + uint8_t tree_sig[64]; member_sync_sign_pubkey(g_sh.ch_ed_priv, inst->my_keys.public_key, tree_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); + join_sig, join_ts, NULL, 0, userinfo, NULL, NULL, 0, 0, tree_sig); } static void member_put_placeholder(struct UTUN_INSTANCE* inst, int idx, const char* name) { @@ -401,8 +446,96 @@ static void rad_tick(void* arg) { 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; + t->phase = P_RADIO_CALLS; break; + case P_RADIO_CALLS: { + uint64_t now = get_time_tb(); + if (g_member_event_bad) { t->result = 2; uasync_stop(t->ua); return; } + switch (t->call_step) { + case 0: { + uint8_t key[32]; + if (member_sync_verified_ed25519(A, CH_ID, nc, key) || member_sync_verified_ed25519(C, CH_ID, na, key)) break; + for (int i = 0; i < 3; ++i) { + g_call_recv[i] = t->inst[i]->router_bindings.callbacks[ETCP_RT_ID_RADIO_CALL]; + t->inst[i]->router_bindings.callbacks[ETCP_RT_ID_RADIO_CALL] = call_packet_loss; + } + g_call_ready = 0; t->call_started = now; + radio_action(A, 100, RADIO_CALL_START, nc); ++t->call_step; + break; + } + case 1: + if (g_calls[0][IDX_A].finished && g_calls[0][IDX_C].finished) { + uint64_t elapsed = g_calls[0][IDX_A].finished_tb - t->call_started; + if (g_calls[0][IDX_A].reason != RADIO_CALL_UNAVAILABLE || elapsed < RADIO_CALL_RESPONSE_TB || + elapsed > RADIO_CALL_RESPONSE_TB + 1000 || g_calls[0][IDX_C].incoming != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test first response deadline failed reason=%d elapsed_tb=%llu incoming=%d", + g_calls[0][IDX_A].reason, (unsigned long long)elapsed, g_calls[0][IDX_C].incoming); + t->result = 2; uasync_stop(t->ua); return; + } + g_call_ready = 1; g_drop_result = 1; g_drop_ack = 1; + radio_action(A, 101, RADIO_CALL_START, nc); ++t->call_step; + } + break; + case 2: + if (g_calls[1][IDX_A].state == RADIO_CALL_RINGING) { + radio_action(C, 101, RADIO_CALL_ACCEPT, 0); ++t->call_step; + } + break; + case 3: + if (g_calls[1][IDX_A].finished && g_calls[1][IDX_C].finished) { + if (g_calls[1][IDX_A].reason != RADIO_CALL_ACCEPTED || !radio_is_active(A, gid) || !radio_is_active(C, gid)) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test radio acceptance failed"); t->result = 2; uasync_stop(t->ua); return; + } + radio_set_active(A, gid, 0); radio_set_active(C, gid, 0); + radio_action(A, 102, RADIO_CALL_START, nc); ++t->call_step; + } + break; + case 4: + if (g_calls[2][IDX_A].state == RADIO_CALL_RINGING) { radio_action(C, 102, RADIO_CALL_DECLINE, 0); ++t->call_step; } + break; + case 5: + if (g_calls[2][IDX_A].finished) { + if (g_calls[2][IDX_A].reason != RADIO_CALL_DECLINED || radio_is_active(A, gid) || radio_is_active(C, gid)) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test radio decline failed"); t->result = 2; uasync_stop(t->ua); return; + } + radio_action(A, 103, RADIO_CALL_START, nc); ++t->call_step; + } + break; + case 6: + if (g_calls[3][IDX_A].state == RADIO_CALL_RINGING) { + radio_action(C, 104, RADIO_CALL_START, nb); + radio_action(A, 103, RADIO_CALL_CANCEL, 0); ++t->call_step; + } + break; + case 7: + if (g_calls[3][IDX_C].finished && g_calls[4][IDX_C].finished) { + if (g_calls[3][IDX_C].reason != RADIO_CALL_CANCELLED || g_calls[4][IDX_C].reason != RADIO_CALL_BUSY) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test radio cancel/busy failed"); t->result = 2; uasync_stop(t->ua); return; + } + t->call_started = now; + radio_action(A, 105, RADIO_CALL_START, nc); ++t->call_step; + } + break; + case 8: + if (g_calls[5][IDX_A].finished && g_calls[5][IDX_C].finished) { + uint64_t elapsed = g_calls[5][IDX_A].finished_tb - t->call_started; + if (g_calls[5][IDX_A].reason != RADIO_CALL_NO_ANSWER || elapsed < RADIO_CALL_RING_TB || elapsed > RADIO_CALL_RING_TB + 1000) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test one minute deadline failed reason=%d elapsed_tb=%llu", + g_calls[5][IDX_A].reason, (unsigned long long)elapsed); t->result = 2; uasync_stop(t->ua); return; + } + for (int n = 0; n < 6; ++n) for (int i = 0; i < 3; ++i) { + if (g_calls[n][i].finished > 1 || g_calls[n][i].incoming > 1) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test duplicated radio call event id=%d node=%d", n + 100, i); + t->result = 2; uasync_stop(t->ua); return; + } + } + DEBUG_INFO(DEBUG_CATEGORY_RADIO, "test radio calls passed: 2s/60s deadlines, accept, decline, cancel, busy, lost result/ACK"); + t->phase = P_RADIO_ON; + } + break; + } + break; + } case P_RADIO_ON: radio_set_active(A, gid, 1); radio_set_active(B, gid, 1);