/* Настоящие Opus/uasync; наблюдаем TX и логи без доступа к private-состоянию call_audio. * Каждый сценарий запускается отдельным процессом, один uasync на поток. */ extern "C" { #include "utun_instance.h" #include "call/call_audio.h" #include "debug_config.h" #include "u_async.h" #include "mem.h" } #include #include #include #include #include #include #include #include #include #define REQUIRE(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call-audio-tx:%d %s", __LINE__, #x); abort(); } } while (0) static constexpr uint64_t old_id = 41, new_id = 42; static std::array pcm{}; static std::array mask{}; static std::atomic test_time{100000}; static unsigned sent_new, sent_old, extra_new, expected_extra, extra_first = 8; static std::vector messages; extern "C" uint64_t __wrap_get_time_tb() { return test_time.load(); } extern "C" int __wrap_chat_setting_get_int(UTUN_INSTANCE*, const char* key, int fallback) { return !strcmp(key, "call_compressor_enabled") || !strcmp(key, "aec_enabled") ? 0 : fallback; } extern "C" int __wrap_call_send_media(UTUN_INSTANCE*, uint64_t id, const uint8_t* data, int len, const call_capture_time* timing) { REQUIRE(data && len > 0 && timing); REQUIRE(id == old_id || id == new_id); if (id == new_id) ++sent_new; else ++sent_old; return 0; } static void capture_diagnostic(int, const char* category, const char* message) { if (category && !strcmp(category, "call")) messages.emplace_back(message); } static bool contains(const char* text) { return std::any_of(messages.begin(), messages.end(), [text](const std::string& s) { return s.find(text) != std::string::npos; }); } static void stats(uint64_t id) { messages.clear(); REQUIRE(call_audio_get_stats(id, nullptr, nullptr, nullptr, nullptr, nullptr) == 0); } static unsigned feed(uint64_t id, unsigned first, unsigned count = 8) { unsigned accepted = 0; for (unsigned i = 0; i < count; ++i) accepted += call_audio_feed_prepared_pcm(id, pcm.data(), mask.data(), int64_t(first + i) * 20000) == 0; return accepted; } static void extra(void*) { stats(new_id); extra_new = feed(new_id, extra_first); DEBUG_INFO(DEBUG_CATEGORY_CALL, "call-audio-tx: queued extra accepted=%u expected=%u", extra_new, expected_extra); REQUIRE(extra_new == expected_extra); } static void begin(UTUN_INSTANCE& inst, uint64_t id) { REQUIRE(call_audio_init(&inst) == 0); REQUIRE(call_audio_start_prepared(id) == 0); } static void unbind(UTUN_INSTANCE& inst) { call_audio_stop(); call_audio_destroy(&inst); } static void drain(UASYNC* ua) { uasync_poll(ua, 0); uasync_poll(ua, 0); /* Tasks, опубликованные из extra, исполняются следующим проходом. */ } int main(int argc, char** argv) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_TRACE); debug_set_log_hook(capture_diagnostic); REQUIRE(argc == 2); const char* mode = argv[1]; size_t allocated = u_get_allocated_count(); UTUN_INSTANCE inst{}, other{}; UTUN_INSTANCE* current = &inst; std::thread worker; std::promise old_ready; std::promise retire_old; if (!strcmp(mode, "cross_loop")) { auto ready = old_ready.get_future(); auto retire = retire_old.get_future(); worker = std::thread([&] { UTUN_INSTANCE old{}; old.ua = uasync_create(); REQUIRE(old.ua); begin(old, old_id); unsigned count = feed(old_id, 0); unbind(old); old_ready.set_value(count); retire.wait(); drain(old.ua); uasync_destroy(old.ua, 0); }); REQUIRE(ready.get() == 8); } UASYNC* ua = uasync_create(); REQUIRE(ua); inst.ua = other.ua = ua; unsigned initial_new = 0, expected_initial = 8; if (!strcmp(mode, "fresh") || !strcmp(mode, "idempotent")) { uasync_post(ua, extra, nullptr); begin(inst, new_id); initial_new = feed(new_id, 0); if (!strcmp(mode, "idempotent")) { REQUIRE(call_audio_init(&inst) == 0); REQUIRE(call_audio_init(&inst) == 0); } } else if (!strcmp(mode, "same_owner") || !strcmp(mode, "reinit") || !strcmp(mode, "mixed_calls")) { unsigned old_count = !strcmp(mode, "mixed_calls") ? 4 : 8; begin(inst, old_id); REQUIRE(feed(old_id, 0, old_count) == old_count); uasync_post(ua, extra, nullptr); call_audio_stop(); if (!strcmp(mode, "reinit")) call_audio_destroy(&inst); begin(inst, new_id); initial_new = feed(new_id, 0); if (strcmp(mode, "reinit")) { expected_initial = 8 - old_count; expected_extra = old_count; } } else if (!strcmp(mode, "different_instance") || !strcmp(mode, "reused_address")) { begin(inst, old_id); REQUIRE(feed(old_id, 0) == 8); uasync_post(ua, extra, nullptr); if (!strcmp(mode, "different_instance")) { call_audio_stop(); current = &other; REQUIRE(call_audio_init(current) == 0); call_audio_destroy(&inst); /* Поздний destroy прежнего владельца не снимает нового. */ } else { unbind(inst); inst = UTUN_INSTANCE{}; inst.ua = ua; inst.node_id = 2; } begin(*current, new_id); initial_new = feed(new_id, 0); } else if (!strcmp(mode, "detached_drain") || !strcmp(mode, "discarded_loop")) { begin(inst, old_id); REQUIRE(feed(old_id, 0) == 8); unbind(inst); if (!strcmp(mode, "detached_drain")) drain(ua); else { uasync_destroy(ua, 0); /* Ожидающие tasks удаляются без callback. */ ua = uasync_create(); REQUIRE(ua); inst.ua = ua; } uasync_post(ua, extra, nullptr); begin(inst, new_id); initial_new = feed(new_id, 0); } else if (!strcmp(mode, "cross_loop")) { uasync_post(ua, extra, nullptr); begin(inst, new_id); initial_new = feed(new_id, 0); retire_old.set_value(); worker.join(); /* Старый loop исполняет tasks после новой привязки. */ } else if (!strcmp(mode, "current_expiry")) { begin(inst, new_id); REQUIRE(feed(new_id, 0) == 8); test_time += 2001; drain(ua); REQUIRE(sent_new == 0); stats(new_id); REQUIRE(contains("posts=8") && contains("TX dropped=8") && contains("age:1")); uasync_post(ua, extra, nullptr); initial_new = feed(new_id, 8); extra_first = 16; } else if (!strcmp(mode, "rebind_storm")) { for (unsigned i = 0; i < 16; ++i) { begin(inst, old_id); REQUIRE(feed(old_id, 0) == 8); unbind(inst); } uasync_post(ua, extra, nullptr); begin(inst, new_id); initial_new = feed(new_id, 0); } else if (!strcmp(mode, "detached_engine")) { begin(inst, old_id); for (unsigned round = 0; round < 2; ++round) { REQUIRE(feed(old_id, round * 8) == 8); stats(old_id); /* Убрать feed-статистику до проверки поздних callbacks. */ call_audio_destroy(&inst); REQUIRE(call_audio_init(&inst) == 0); /* Encoder и call generation пока не меняются. */ if (round) test_time += 2001; drain(ua); DEBUG_INFO(DEBUG_CATEGORY_CALL, "call-audio-tx: detached encoder round=%u sent_old=%u", round, sent_old); REQUIRE(sent_old == 0); REQUIRE(contains("owner=0")); stats(old_id); REQUIRE(!contains("TX timing") && !contains("TX dropped=")); } call_audio_stop(); uasync_post(ua, extra, nullptr); begin(inst, new_id); initial_new = feed(new_id, 0); } else { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call-audio-tx: unknown scenario=%s", mode); abort(); } REQUIRE(initial_new == expected_initial); drain(ua); REQUIRE(extra_new == expected_extra && sent_new == 8 && sent_old == 0); stats(new_id); REQUIRE(contains("posts=8") && contains("pending=0")); /* После drain разрешены ровно восемь новых задач; девятая снова отклоняется. */ REQUIRE(feed(new_id, extra_first + 8) == 8); REQUIRE(feed(new_id, extra_first + 16, 1) == 0); unbind(*current); uasync_destroy(ua, 0); /* И этот финальный хвост освобождается без callback. */ REQUIRE(sent_new == 8 && sent_old == 0); size_t remaining = u_get_allocated_count(); REQUIRE(remaining == allocated); debug_set_log_hook(nullptr); DEBUG_INFO(DEBUG_CATEGORY_CALL, "call-audio-tx: scenario=%s initial=%u extra=%u sent_new=%u sent_old=%u allocations=%zu->%zu PASS", mode, initial_new, extra_new, sent_new, sent_old, allocated, remaining); return 0; }