Route MainWindow radio invitations through owned commands and test them
This commit is contained in:
1 parent
0f36248d1e
commit
bc78e0889a
3 files changed
+242
-17
No files matched your search
@@ -580,6 +580,20 @@ endif()
|
||||
add_test(NAME test_settings_save COMMAND test_settings_save)
|
||||
set_tests_properties(test_settings_save PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_SESSION_TYPE=x11" TIMEOUT 60)
|
||||
|
||||
add_executable(test_radio_commands tests/test_radio_commands.cpp ${DM_DELETION_SOURCES})
|
||||
add_dependencies(test_radio_commands chatgui_translations)
|
||||
target_include_directories(test_radio_commands PRIVATE $<TARGET_PROPERTY:vibechat,INCLUDE_DIRECTORIES>)
|
||||
target_compile_definitions(test_radio_commands PRIVATE $<TARGET_PROPERTY:vibechat,COMPILE_DEFINITIONS>)
|
||||
target_link_libraries(test_radio_commands PRIVATE ${DM_DELETION_LIBRARIES} ${QT_TEST_LIB})
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
target_compile_definitions(test_radio_commands PRIVATE UTUN_TEST_RADIO_HOOKS)
|
||||
foreach(symbol u_malloc_impl u_calloc_impl u_free_impl radio_call_trampoline ma_context_init)
|
||||
target_link_options(test_radio_commands PRIVATE "-Wl,--wrap=${symbol}")
|
||||
endforeach()
|
||||
endif()
|
||||
add_test(NAME test_radio_commands COMMAND test_radio_commands)
|
||||
set_tests_properties(test_radio_commands PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_SESSION_TYPE=x11" TIMEOUT 30)
|
||||
|
||||
add_executable(test_group_edits tests/test_group_edits.cpp ${DM_DELETION_SOURCES})
|
||||
add_dependencies(test_group_edits chatgui_translations)
|
||||
target_include_directories(test_group_edits PRIVATE $<TARGET_PROPERTY:vibechat,INCLUDE_DIRECTORIES>)
|
||||
|
||||
@@ -811,20 +811,13 @@ void MainWindow::showMemberFiles(const QString& channel, quint64 owner) {
|
||||
m_fileBrowser->setChannel(channel, owner); m_fileBrowser->show(); m_fileBrowser->raise(); m_fileBrowser->activateWindow();
|
||||
}
|
||||
|
||||
/* Все команды рации идут в поток ядра; GUI не читает живые таблицы BGP. */
|
||||
static void postRadioCall(quint64 id, int action, quint64 group = 0, quint64 peer = 0) {
|
||||
auto* inst = gui_bridge_get_inst();
|
||||
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "desktop radio call without running core"); return; }
|
||||
auto* req = static_cast<radio_call_request*>(u_calloc(1, sizeof(radio_call_request)));
|
||||
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "desktop radio call request allocation failed"); return; }
|
||||
req->inst = inst; req->id = id; req->group = group; req->peer = peer; req->action = action;
|
||||
gui_bridge_post_uasync_fn(radio_call_trampoline, req);
|
||||
}
|
||||
|
||||
/* GUI передаёт мосту значения; ядро и время жизни запроса выбирает мост. */
|
||||
void MainWindow::onRadioCallRequested(const QString& channel, quint64 peer) {
|
||||
if (m_radioCallWindow) { m_radioCallWindow->show(); m_radioCallWindow->raise(); return; }
|
||||
quint64 id = QRandomGenerator::global()->generate64();
|
||||
postRadioCall(id ? id : 1, RADIO_CALL_START, channel.toULongLong(), peer);
|
||||
if (gui_bridge_radio_call(GUI_RADIO_CALL_START, id ? id : 1, channel.toULongLong(), peer) < 0)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "desktop radio call start was not queued channel=%s peer=%llu",
|
||||
qPrintable(channel), (unsigned long long)peer);
|
||||
}
|
||||
|
||||
/* Один диалог на незавершённое приглашение; принятая рация живёт своим обычным циклом. */
|
||||
@@ -878,7 +871,7 @@ void MainWindow::onRadioCallEvent(const uint8_t* data, int len) {
|
||||
m_radioCallWindow = nullptr;
|
||||
}
|
||||
if (m_radioCallId == id) return;
|
||||
if (m_radioCallId || !m_activeCalls.isEmpty()) { postRadioCall(id, RADIO_CALL_REJECT_BUSY); return; }
|
||||
if (m_radioCallId || !m_activeCalls.isEmpty()) { gui_bridge_radio_call(GUI_RADIO_CALL_REJECT_BUSY, id, 0, 0); return; }
|
||||
QString name = m_db->getDisplayName(peer);
|
||||
if (name.isEmpty()) name = QStringLiteral("0x%1").arg(peer, 16, 16, QChar('0'));
|
||||
QString channelName = channel;
|
||||
@@ -894,11 +887,11 @@ void MainWindow::onRadioCallEvent(const uint8_t* data, int len) {
|
||||
connect(box, &QMessageBox::buttonClicked, this, [this, id, channel, accept](QAbstractButton* button) {
|
||||
if (m_radioCallId != id) return;
|
||||
m_radioCallAnswered = true;
|
||||
if (button != accept) { postRadioCall(id, RADIO_CALL_DECLINE); return; }
|
||||
if (button != accept) { gui_bridge_radio_call(GUI_RADIO_CALL_DECLINE, id, 0, 0); return; }
|
||||
SoundManager::instance()->stopLoop();
|
||||
m_channelList->selectChannel(channel);
|
||||
bool ready = m_currentChannelId == channel && m_messageList->enableRadio();
|
||||
postRadioCall(id, ready ? RADIO_CALL_ACCEPT : RADIO_CALL_FAIL);
|
||||
gui_bridge_radio_call(ready ? GUI_RADIO_CALL_ACCEPT : GUI_RADIO_CALL_FAIL, id, 0, 0);
|
||||
});
|
||||
} else if (state == RADIO_CALL_WAIT_DECISION) {
|
||||
box->setInformativeText(QCoreApplication::translate("ChatGui", "Subscriber is on a phone call"));
|
||||
@@ -906,10 +899,10 @@ void MainWindow::onRadioCallEvent(const uint8_t* data, int len) {
|
||||
box->addButton(QCoreApplication::translate("ChatGui", "End call"), QMessageBox::RejectRole);
|
||||
connect(box, &QMessageBox::buttonClicked, this, [this, id, proceed](QAbstractButton* button) {
|
||||
if (m_radioCallId != id || button != proceed) return;
|
||||
if (gui_bridge_radio_call(GUI_RADIO_CALL_CONTINUE, id, 0, 0) < 0) return;
|
||||
m_radioCallAnswered = true;
|
||||
m_radioCallId = 0;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "desktop caller overrides phone busy id=%016llx", (unsigned long long)id);
|
||||
postRadioCall(id, RADIO_CALL_CONTINUE);
|
||||
});
|
||||
} else {
|
||||
box->addButton(QMessageBox::Cancel);
|
||||
@@ -919,7 +912,7 @@ void MainWindow::onRadioCallEvent(const uint8_t* data, int len) {
|
||||
if (m_radioCallId == id && !m_radioCallAnswered) {
|
||||
m_radioCallAnswered = true;
|
||||
SoundManager::instance()->stopLoop();
|
||||
postRadioCall(id, incoming ? RADIO_CALL_DECLINE : RADIO_CALL_CANCEL);
|
||||
gui_bridge_radio_call(incoming ? GUI_RADIO_CALL_DECLINE : GUI_RADIO_CALL_CANCEL, id, 0, 0);
|
||||
}
|
||||
});
|
||||
box->show(); box->raise(); box->activateWindow();
|
||||
@@ -927,7 +920,7 @@ void MainWindow::onRadioCallEvent(const uint8_t* data, int len) {
|
||||
if (incoming) {
|
||||
if (m_trayIcon && (!isVisible() || isMinimized()))
|
||||
m_trayIcon->showMessage(QCoreApplication::translate("ChatGui", "Radio call"), box->text());
|
||||
postRadioCall(id, RADIO_CALL_READY);
|
||||
gui_bridge_radio_call(GUI_RADIO_CALL_READY, id, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/* Настоящие 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"
|
||||
Reference in new issue
Block a user