/* Формат канального события: live/history сохраняются при передаче в GUI, битые события отбрасываются. */ #include "../transport/gui_bridge.h" #include "../../../lib/debug_config.h" #include "../../../src/chat/chat_event.h" #include "../../../src/chat/chat_core.h" #include "../../../src/utun_instance.h" #include "../../../lib/u_async.h" #include "../../../lib/mem.h" #include #include #include #include #ifdef UTUN_TEST_BRIDGE_ALLOC_FAILURES static int failAllocation; extern "C" void* __real_u_calloc_impl(uint32_t, uint32_t, const char*); extern "C" void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* where) { if (failAllocation && strstr(where, "gui_bridge_impl.cpp") && --failAllocation == 0) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "[INJECT] GUI bridge allocation failure at %s", where); return nullptr; } return __real_u_calloc_impl(count, size, where); } #endif struct ReceivedMessage { QString channel; quint64 author; bool initialSync; bool guiThread; }; static QList received; static QList cpuLoads; static bool cpuOnGuiThread = true; static QStringList changedChannels; static QList edits; static int readyEvents; struct BoundCore { UASYNC* ua = uasync_create(); UTUN_INSTANCE instance{}; ~BoundCore() { gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); if (ua) { uasync_mark_stopped(ua); uasync_destroy(ua, 0); } } void attach() { instance.ua = ua; uasync_mark_running(ua); gui_bridge_set_uasync(ua); gui_bridge_set_inst(&instance); } }; static void cpu_received(int percent) { cpuLoads.append(percent); cpuOnGuiThread &= QThread::currentThread() == qApp->thread(); } static void message_received(const char* channel, int length, uint64_t author, int initial_sync) { received.append({QString::fromUtf8(channel, length), author, initial_sync != 0, QThread::currentThread() == qApp->thread()}); } static QByteArray message_event(quint64 author, bool initialSync) { const QByteArray channel("12345"); QByteArray event(1 + channel.size() + 9, '\0'); event[0] = char(channel.size()); memcpy(event.data() + 1, channel.constData(), channel.size()); memcpy(event.data() + 1 + channel.size(), &author, 8); event[event.size() - 1] = initialSync; return event; } static void post_message(const QByteArray& event) { gui_bridge_post(GUI_EVT_MSG_RECEIVED, reinterpret_cast(event.constData()), event.size()); } class MessageEventTest : public QObject { Q_OBJECT private slots: void initTestCase() { debug_config_init(); debug_set_level(DEBUG_LEVEL_DEBUG); gui_bridge_init(); gui_bridge_set_msg_received_cb(message_received); } void cleanupTestCase() { gui_bridge_set_msg_received_cb(nullptr); gui_bridge_set_cpu_load_cb(nullptr); } void init() { received.clear(); } void liveAndHistory() { post_message(message_event(42, false)); post_message(message_event(43, true)); QTRY_COMPARE(received.size(), 2); QCOMPARE(received[0].channel, QString("12345")); QCOMPARE(received[0].author, quint64(42)); QVERIFY(!received[0].initialSync); QVERIFY(received[0].guiThread); QCOMPARE(received[1].author, quint64(43)); QVERIFY(received[1].initialSync); QVERIFY(received[1].guiThread); } void rejectsMalformedEvents() { QByteArray event = message_event(42, false); event.chop(1); post_message(event); event = message_event(42, false); event[event.size() - 1] = 2; post_message(event); event = message_event(42, false); event[0] = char(255); post_message(event); post_message({}); QTest::qWait(100); QVERIFY(received.isEmpty()); } void editIsDeliveredSeparatelyFromNewMessage() { edits.clear(); gui_bridge_set_msg_updated_cb([](const uint8_t* data, int len) { QVERIFY(QThread::currentThread() == qApp->thread()); edits.append(QByteArray((const char*)data, len)); }); QByteArray event(1, char(5)); event += "12345"; event += QByteArray(32, 'k'); gui_bridge_post(CHAT_EVT_MSG_UPDATED, (const uint8_t*)event.constData(), event.size()); QTRY_COMPARE(edits.size(), 1); QCOMPARE(edits.first(), event); QVERIFY(received.isEmpty()); event.chop(1); gui_bridge_post(CHAT_EVT_MSG_UPDATED, (const uint8_t*)event.constData(), event.size()); QTest::qWait(30); QCOMPARE(edits.size(), 1); QVERIFY(received.isEmpty()); gui_bridge_set_msg_updated_cb(nullptr); } void cpuLoadDeliveredToGuiThread() { cpuLoads.clear(); cpuOnGuiThread = true; gui_bridge_set_cpu_load_cb(cpu_received); std::thread worker([] { const uint8_t samples[][2] = {{1, 0}, {1, 50}, {1, 100}, {0, 90}, {1, 101}}; for (const auto& sample : samples) gui_bridge_post(CHAT_EVT_CPU_LOAD, sample, sizeof(sample)); gui_bridge_post(CHAT_EVT_CPU_LOAD, nullptr, 0); }); worker.join(); QVERIFY(cpuLoads.isEmpty()); // Worker не вызывает UI напрямую. QTRY_COMPARE(cpuLoads.size(), 6); QCOMPARE(cpuLoads, QList({0, 50, 100, 0, 0, 0})); QVERIFY(cpuOnGuiThread); gui_bridge_set_cpu_load_cb(nullptr); const uint8_t sample[] = {1, 75}; gui_bridge_post(CHAT_EVT_CPU_LOAD, sample, sizeof(sample)); QCoreApplication::processEvents(); QCOMPARE(cpuLoads.size(), 6); } void deletionRefreshesChannelsOnlyAfterValidEvent() { changedChannels.clear(); gui_bridge_set_channel_updated_cb([](const char* channel, int length) { QVERIFY(QThread::currentThread() == qApp->thread()); changedChannels.append(QString::fromUtf8(channel, length)); }); std::thread worker([] { const uint8_t valid[] = {3, '1', '2', '3'}; gui_bridge_post(CHAT_EVT_CHANNEL_DELETED, valid, sizeof(valid)); gui_bridge_post(CHAT_EVT_CHANNEL_DELETED, valid, sizeof(valid) - 1); const uint8_t empty[] = {0}; gui_bridge_post(CHAT_EVT_CHANNEL_DELETED, empty, sizeof(empty)); gui_bridge_post(CHAT_EVT_CHANNEL_DELETED, nullptr, 0); }); worker.join(); QTRY_COMPARE(changedChannels, QStringList({"123"})); QCoreApplication::processEvents(); QCOMPARE(changedChannels.size(), 1); gui_bridge_set_channel_updated_cb(nullptr); } void shutdownCancelsOwnedCommandsAndKeepsBorrowedArguments() { const size_t before = u_get_allocated_count(); { BoundCore core; QVERIFY(core.ua); core.attach(); const size_t baseline = u_get_allocated_count(); auto* req = static_cast(u_calloc(1, sizeof(chat_msg_submit))); QVERIFY(req); QCOMPARE(gui_bridge_submit_message(req), 0); QCOMPARE(gui_bridge_delete_dm("123", 1, 1), 0); QCOMPARE(gui_bridge_toggle_member_radio_mute("123", 7), 0); QCOMPARE(gui_bridge_request_cpu_load(), 1); int calls = 0; gui_bridge_post_uasync_fn([](void* p) { ++*static_cast(p); }, &calls); gui_bridge_set_inst(nullptr); QVERIFY(!gui_bridge_is_uasync_ready()); uasync_poll(core.ua, 0); QCOMPARE(calls, 0); QCOMPARE(u_get_allocated_count(), baseline); } QCOMPARE(u_get_allocated_count(), before); } void invalidPostArgumentsDoNotTurnIntoEmptyEvents() { readyEvents = 0; gui_bridge_set_db_ready_cb([] { readyEvents++; }); gui_bridge_post(GUI_EVT_DB_READY, nullptr, -1); gui_bridge_post(GUI_EVT_DB_READY, nullptr, 10); QCoreApplication::processEvents(); gui_bridge_set_db_ready_cb(nullptr); QCOMPARE(readyEvents, 0); } void repeatedInitKeepsQueuedEventsAndJoinRequiresOutput() { post_message(message_event(42, false)); gui_bridge_init(); QTRY_COMPARE(received.size(), 1); QCOMPARE(received[0].author, quint64(42)); QVERIFY(gui_bridge_join_link("", 0, nullptr) < 0); QVERIFY(gui_bridge_offer_link(1, "", 0, nullptr) < 0); uint64_t request = 42; QVERIFY(gui_bridge_join_link("", 0, &request) < 0); QCOMPARE(request, uint64_t(42)); QVERIFY(gui_bridge_offer_link(1, "", 0, &request) < 0); QCOMPARE(request, uint64_t(42)); } void ownedPostsDeliverOrCancelAndRejectedArgumentsStayWithCaller() { const size_t before = u_get_allocated_count(); { BoundCore core; QVERIFY(core.ua); core.attach(); const size_t baseline = u_get_allocated_count(); auto* value = static_cast(u_malloc(sizeof(int))); QVERIFY(value); *value = 7; QCOMPARE(gui_bridge_post_uasync_owned_fn([](void* p) { QCOMPARE(*static_cast(p), 7); u_free(p); }, value), 0); uasync_poll(core.ua, 0); QCOMPARE(u_get_allocated_count(), baseline); value = static_cast(u_malloc(sizeof(int))); QVERIFY(value); QCOMPARE(gui_bridge_post_uasync_owned_fn([](void*) { QFAIL("cancelled command executed"); }, value), 0); gui_bridge_set_inst(nullptr); QCOMPARE(u_get_allocated_count(), baseline); value = static_cast(u_malloc(sizeof(int))); QVERIFY(value); QVERIFY(gui_bridge_post_uasync_owned_fn([](void*) {}, value) < 0); QCOMPARE(u_get_allocated_count(), baseline + 1); u_free(value); core.attach(); QCOMPARE(gui_bridge_request_cpu_load(), 1); // Отмена прежнего запуска не повреждает очередь следующего. } QCOMPARE(u_get_allocated_count(), before); } void concurrentProducerCanOutliveCoreDetachAndUasyncDestroy() { const size_t before = u_get_allocated_count(); BoundCore core; QVERIFY(core.ua); core.attach(); const auto general = g_debug_config.category_levels[DEBUG_CATEGORY_GENERAL]; const auto sys = g_debug_config.category_levels[DEBUG_CATEGORY_SYS]; debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DISABLED); debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_WARN); std::promise first; auto queued = first.get_future(); unsigned accepted = 0, rejected = 0; bool allocated = true; std::thread producer([&] { for (unsigned i = 0; i < 128; i++) { void* arg = u_malloc(1); if (!arg) { allocated = false; if (!i) first.set_value(); break; } if (gui_bridge_post_uasync_owned_fn([](void* p) { u_free(p); }, arg) == 0) accepted++; else { rejected++; u_free(arg); } if (!i) first.set_value(); } }); queued.wait(); gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); uasync_mark_stopped(core.ua); uasync_destroy(core.ua, 0); core.ua = nullptr; producer.join(); debug_set_category_level(DEBUG_CATEGORY_GENERAL, general); debug_set_category_level(DEBUG_CATEGORY_SYS, sys); QVERIFY(allocated); QCOMPARE(accepted + rejected, 128u); QCOMPARE(u_get_allocated_count(), before); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[PASS] bridge detach race: accepted=%u rejected=%u, producer joined, no leaks", accepted, rejected); } #ifdef UTUN_TEST_BRIDGE_ALLOC_FAILURES void allocationFailuresKeepOwnershipAndReleasePartialRequests() { BoundCore core; QVERIFY(core.ua); core.attach(); const size_t baseline = u_get_allocated_count(); auto* req = static_cast(u_calloc(1, sizeof(chat_msg_submit))); QVERIFY(req); failAllocation = 1; QVERIFY(gui_bridge_submit_message(req) < 0); QCOMPARE(failAllocation, 0); QCOMPARE(u_get_allocated_count(), baseline + 1); u_free(req); for (int point : {1, 2}) { failAllocation = point; QVERIFY(gui_bridge_delete_dm("123", 1, 1) < 0); QCOMPARE(failAllocation, 0); QCOMPARE(u_get_allocated_count(), baseline); failAllocation = point; QVERIFY(gui_bridge_toggle_member_radio_mute("123", 7) < 0); QCOMPARE(failAllocation, 0); QCOMPARE(u_get_allocated_count(), baseline); } failAllocation = 1; QCOMPARE(gui_bridge_request_cpu_load(), 0); QCOMPARE(failAllocation, 0); QCOMPARE(u_get_allocated_count(), baseline); } #endif }; QTEST_MAIN(MessageEventTest) #include "test_message_events.moc"