285 lines
13 KiB
C++
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"
|