219 lines
11 KiB
C++
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"
|