Files
utun2/tools/chatgui/tests/test_message_events.cpp
T

285 lines
13 KiB
C++

/* Формат канального события: 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 <QtTest>
#include <cstring>
#include <thread>
#include <future>
#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<ReceivedMessage> received;
static QList<int> cpuLoads;
static bool cpuOnGuiThread = true;
static QStringList changedChannels;
static QList<QByteArray> 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<const uint8_t*>(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<int>({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<chat_msg_submit*>(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<int*>(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<int*>(u_malloc(sizeof(int))); QVERIFY(value); *value = 7;
QCOMPARE(gui_bridge_post_uasync_owned_fn([](void* p) {
QCOMPARE(*static_cast<int*>(p), 7); u_free(p);
}, value), 0);
uasync_poll(core.ua, 0); QCOMPARE(u_get_allocated_count(), baseline);
value = static_cast<int*>(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<int*>(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<void> 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<chat_msg_submit*>(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"