Delete local conversations atomically, retain replay protection and sequence counters, and guard asynchronous media work by history generation. Add Android avatar menu and confirmation, persistent file cleanup, and regression coverage. Fix unnamed member wire padding and compact Android member action spacing. Validation: 158 core tests passed; targeted DM tests, desktop build, Android unit tests and debug APK build passed.
658 lines
27 KiB
C
658 lines
27 KiB
C
// 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 "test_clock.h"
|
|
#include "test_net.h"
|
|
|
|
#include <sqlite3.h>
|
|
#define OPENSSL_API_COMPAT 0x10100000L
|
|
#include <openssl/evp.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
#include <time.h>
|
|
|
|
#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];
|
|
};
|
|
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(void) {
|
|
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]);
|
|
}
|
|
{ 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:0\n", apub);
|
|
} 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:0\n", bpub);
|
|
}
|
|
|
|
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:0\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, 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");
|
|
}
|
|
test_clock_init(TICK_TB);
|
|
test_clock_sync_wall_time();
|
|
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();
|
|
write_configs(dir);
|
|
|
|
printf("=== test_radio ===\n"); fflush(stdout);
|
|
|
|
TEST("member snapshot: short names have zero padding"); {
|
|
const char* names[] = {"", "0x06B9", "Без имени", "123456789012345678901234567890123456789012345678901234567890123"};
|
|
int valid = 1;
|
|
for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
|
|
struct chat_member_display member = {0};
|
|
member.node_id = UINT64_C(0x212732e6bc9c06b9);
|
|
memset(member.name, 0xFF, sizeof(member.name));
|
|
snprintf(member.name, sizeof(member.name), "%s", names[i]);
|
|
uint8_t wire[CHAT_MEMBER_DISPLAY_SIZE];
|
|
chat_core_serialize_member(&member, wire);
|
|
char expected[64] = {0};
|
|
snprintf(expected, sizeof(expected), "%s", names[i]);
|
|
if (memcmp(wire + 15, expected, sizeof(expected))) { valid = 0; break; }
|
|
}
|
|
if (valid) OK(); else FAIL("name field contains bytes after NUL");
|
|
}
|
|
|
|
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;
|
|
}
|
|
test_etcp_set_peer(t.inst[IDX_B], "to_a", t.inst[IDX_A], "s1");
|
|
test_etcp_set_peer(t.inst[IDX_C], "to_b", t.inst[IDX_B], "s1");
|
|
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");
|
|
test_clock_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);
|
|
test_clock_finish();
|
|
return G_FAILED > 0 ? 1 : 0;
|
|
}
|