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

219 lines
11 KiB
C++

/* Настоящие MainWindow/bridge/radio/uasync: отказ, отмена, OOM и привязка команды к владельцу очереди.
* Native trampoline исполняется без подмены; пустой каталог CHAT даёт штатный FINISHED/UNAVAILABLE для START. */
#include "../src/mainwindow.h"
#include "../transport/gui_bridge.h"
#include <QtTest>
#include <QTemporaryDir>
#include <QFile>
#include <atomic>
#include <thread>
#include <mutex>
#include <map>
#include <memory>
#include <cstring>
extern "C" {
#include "utun_instance.h"
#include "chat/chat_event.h"
#include "radio/radio_call.h"
#include "mem.h"
#include "debug_config.h"
}
static UTUN_INSTANCE* executingCore = nullptr;
static bool bindingValid = true;
static QList<radio_call_request> observed;
#ifdef UTUN_TEST_RADIO_HOOKS
#include "miniaudio.h"
static std::atomic<bool> watching{false};
static std::atomic<int> failAllocation{0};
static std::mutex allocationMutex;
static std::map<void*, const char*> outstanding;
static void track(void* pointer, const char* source) {
if (!pointer || !watching || (!strstr(source, "/gui_bridge_impl.cpp:") && !strstr(source, "/mainwindow.cpp:"))) return;
std::lock_guard<std::mutex> lock(allocationMutex); outstanding.emplace(pointer, source);
}
extern "C" void* __real_u_malloc_impl(uint32_t, const char*);
extern "C" void* __wrap_u_malloc_impl(uint32_t bytes, const char* source) {
void* pointer = __real_u_malloc_impl(bytes, source); track(pointer, source); return pointer;
}
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 bytes, const char* source) {
if (failAllocation && strstr(source, "/gui_bridge_impl.cpp:") && --failAllocation == 0) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "[INJECT] radio command allocation failure at %s", source); return nullptr;
}
void* pointer = __real_u_calloc_impl(count, bytes, source); track(pointer, source); return pointer;
}
extern "C" void __real_u_free_impl(void*, const char*);
extern "C" void __wrap_u_free_impl(void* pointer, const char* source) {
{ std::lock_guard<std::mutex> lock(allocationMutex); outstanding.erase(pointer); }
__real_u_free_impl(pointer, source);
}
extern "C" void __real_radio_call_trampoline(void*);
extern "C" void __wrap_radio_call_trampoline(void* arg) {
auto* request = static_cast<radio_call_request*>(arg);
bindingValid &= request->inst == executingCore; observed.append(*request);
__real_radio_call_trampoline(arg);
}
// Настоящий mixer остаётся включённым; null backend устраняет зависимость от аппаратных устройств.
extern "C" ma_result __real_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context*);
extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config* config, ma_context* context) {
const ma_backend backend = ma_backend_null;
return __real_ma_context_init(&backend, 1, config, context);
}
#endif
struct RadioCore {
QTemporaryDir directory;
UASYNC* ua = uasync_create();
UTUN_INSTANCE* inst = nullptr;
unsigned results = 0;
bool resultValid = true;
~RadioCore() {
gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr);
if (ua) uasync_mark_stopped(ua);
if (inst) utun_instance_destroy(inst);
if (ua) uasync_destroy(ua, 0);
}
bool start() {
if (!ua || !directory.isValid()) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "fixture: no uasync/config directory"); return false;
}
const QByteArray config = "[global]\nauto_sockets=no\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n"
"member_files_enabled=0\nlocal_invites_enabled=0\nlocal_broadcasts_enabled=0\n[ntp]\nenabled=no\n";
const QString path = directory.filePath("utun.cfg"); QFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(config) != config.size()) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "fixture: config write failed: %s", qPrintable(file.errorString())); return false;
}
file.close(); inst = utun_instance_create(ua, path.toUtf8().constData());
if (!inst || utun_core_start(inst) || chat_service_start(inst)) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "fixture: core/chat startup failed"); return false;
}
inst->test_user_ptr = this;
chat_event_set_handler(inst, [](UTUN_INSTANCE* instance, int type, const uint8_t* data, int len) {
if (type != CHAT_EVT_RADIO_CALL) return;
auto* core = static_cast<RadioCore*>(instance->test_user_ptr); ++core->results;
core->resultValid &= len == 27 && data[24] == RADIO_CALL_FINISHED && data[25] == RADIO_CALL_UNAVAILABLE && !data[26];
});
attach(); poll(); return true;
}
void attach() { uasync_mark_running(ua); gui_bridge_set_uasync(ua); gui_bridge_set_inst(inst); }
void poll() { executingCore = inst; uasync_poll(ua, 0); executingCore = nullptr; }
};
static unsigned pending(UASYNC* ua) {
unsigned count = 0;
for (auto* task = ua->posted_tasks_head; task; task = task->next) ++count;
return count;
}
class RadioCommandsTest : public QObject {
Q_OBJECT
private slots:
void initTestCase() {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); gui_bridge_init();
debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG);
}
void init() {
observed.clear(); bindingValid = true;
#ifdef UTUN_TEST_RADIO_HOOKS
watching = false; failAllocation = 0;
#endif
}
void cleanup() {
#ifdef UTUN_TEST_RADIO_HOOKS
failAllocation = 0;
#endif
gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr);
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
#ifdef UTUN_TEST_RADIO_HOOKS
watching = false;
std::lock_guard<std::mutex> lock(allocationMutex);
for (const auto& item : outstanding)
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Unfreed radio command ptr=%p source=%s", item.first, item.second);
QCOMPARE(outstanding.size(), size_t(0));
#endif
}
void lifecycle_data() {
QTest::addColumn<int>("action"); QTest::addColumn<QString>("scenario"); QTest::addColumn<bool>("window");
QStringList scenarios{"unavailable", "stopped", "cancelled", "executed"};
#ifdef UTUN_TEST_RADIO_HOOKS
scenarios << "request-oom" << "queue-oom";
#endif
for (int action = GUI_RADIO_CALL_START; action <= GUI_RADIO_CALL_CONTINUE; ++action)
for (const auto& scenario : scenarios)
QTest::newRow(qPrintable(QString("bridge-%1-%2").arg(action).arg(scenario))) << action << scenario << false;
for (const auto& scenario : scenarios)
QTest::newRow(qPrintable("window-" + scenario)) << int(GUI_RADIO_CALL_START) << scenario << true;
QTest::newRow("window-destroyed-before-execution") << int(GUI_RADIO_CALL_START) << QString("destroyed") << true;
}
void lifecycle() {
QFETCH(int, action); QFETCH(QString, scenario); QFETCH(bool, window);
RadioCore core; QVERIFY(core.start());
std::unique_ptr<MainWindow> form;
if (window) form = std::make_unique<MainWindow>(nullptr, nullptr, QString()); // UtunNode остаётся владельцу fixture.
if (scenario == "unavailable") gui_bridge_set_inst(nullptr);
if (scenario == "stopped") uasync_mark_stopped(core.ua);
#ifdef UTUN_TEST_RADIO_HOOKS
watching = true; failAllocation = scenario == "request-oom" ? 1 : scenario == "queue-oom" ? 2 : 0;
#endif
const bool accepted = scenario == "executed" || scenario == "cancelled" || scenario == "destroyed";
const uint64_t group = action == GUI_RADIO_CALL_START ? 7654321 : 0, peer = action == GUI_RADIO_CALL_START ? 42 : 0;
if (form) form->onRadioCallRequested(QString::number(group), peer);
else QCOMPARE(gui_bridge_radio_call(static_cast<gui_radio_call_action>(action), 73, group, peer), accepted ? 0 : -1);
QCOMPARE(pending(core.ua), accepted ? 1u : 0u);
if (scenario == "destroyed") form.reset();
const bool executes = scenario == "executed" || scenario == "destroyed";
if (executes) core.poll();
else gui_bridge_set_inst(nullptr);
QCOMPARE(pending(core.ua), 0u); QVERIFY(bindingValid);
QCOMPARE(core.results, executes && action == GUI_RADIO_CALL_START ? 1u : 0u); QVERIFY(core.resultValid);
#ifdef UTUN_TEST_RADIO_HOOKS
QCOMPARE(failAllocation.load(), 0); QCOMPARE(observed.size(), executes ? 1 : 0);
if (executes) {
static const int nativeActions[]{RADIO_CALL_START, RADIO_CALL_READY, RADIO_CALL_ACCEPT, RADIO_CALL_DECLINE,
RADIO_CALL_CANCEL, RADIO_CALL_FAIL, RADIO_CALL_REJECT_BUSY, RADIO_CALL_CONTINUE};
const auto& request = observed.front(); QCOMPARE(request.inst, core.inst);
QCOMPARE(request.action, nativeActions[action]); QCOMPARE(request.group, group); QCOMPARE(request.peer, peer);
if (window) QVERIFY(request.id);
else QCOMPARE(request.id, uint64_t(73));
}
#endif
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "Radio command verified: window=%d action=%d scenario=%s results=%u",
window, action, qPrintable(scenario), core.results);
}
void invalidArguments() {
RadioCore core; QVERIFY(core.start()); const auto baseline = u_get_allocated_count();
QCOMPARE(gui_bridge_radio_call(static_cast<gui_radio_call_action>(99), 1, 0, 0), -1);
QCOMPARE(gui_bridge_radio_call(GUI_RADIO_CALL_START, 0, 1, 1), -1);
QCOMPARE(gui_bridge_radio_call(GUI_RADIO_CALL_START, 1, 0, 1), -1);
QCOMPARE(gui_bridge_radio_call(GUI_RADIO_CALL_START, 1, 1, 0), -1);
QCOMPARE(gui_bridge_radio_call(GUI_RADIO_CALL_CANCEL, 0, 0, 0), -1);
QCOMPARE(gui_bridge_radio_call(GUI_RADIO_CALL_CANCEL, 1, 1, 0), -1);
QCOMPARE(gui_bridge_radio_call(GUI_RADIO_CALL_CANCEL, 1, 0, 1), -1);
QCOMPARE(pending(core.ua), 0u); QCOMPARE(u_get_allocated_count(), baseline);
}
void concurrentRebind() {
RadioCore first, second; QVERIFY(first.start()); gui_bridge_set_inst(nullptr); QVERIFY(second.start());
#ifdef UTUN_TEST_RADIO_HOOKS
watching = true;
#endif
std::thread producer([] {
for (uint64_t id = 1; id <= 96; ++id) gui_bridge_radio_call(GUI_RADIO_CALL_CANCEL, id, 0, 0);
});
for (unsigned i = 0; i < 64; ++i) {
gui_bridge_set_inst(nullptr); auto& core = i % 2 ? first : second; core.attach(); core.poll();
}
producer.join(); gui_bridge_set_inst(nullptr); first.attach();
QCOMPARE(gui_bridge_radio_call(GUI_RADIO_CALL_START, 1000, 7654321, 42), 0); first.poll();
QVERIFY(bindingValid); QCOMPARE(pending(first.ua), 0u); QCOMPARE(pending(second.ua), 0u);
QCOMPARE(first.results, 1u); QCOMPARE(second.results, 0u); QVERIFY(first.resultValid);
#ifdef UTUN_TEST_RADIO_HOOKS
QVERIFY(!observed.isEmpty()); QCOMPARE(observed.back().inst, first.inst); QCOMPARE(observed.back().id, uint64_t(1000));
#endif
}
};
QTEST_MAIN(RadioCommandsTest)
#include "test_radio_commands.moc"