From e23a7e27eb887e24757c5faf24fc08a247ce2c88 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 21:45:23 +0100 Subject: [PATCH] Test call command ownership, allocation failures and core rebinding --- tools/chatgui/CMakeLists.txt | 13 ++ tools/chatgui/tests/test_call_commands.cpp | 222 +++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 tools/chatgui/tests/test_call_commands.cpp diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 1e9ec1958..00f36b565 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -532,6 +532,19 @@ endif() add_test(NAME test_message_events COMMAND test_message_events) set_tests_properties(test_message_events PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 15) +add_executable(test_call_commands tests/test_call_commands.cpp src/callwindow.cpp src/call_audio_engine.cpp src/sound_manager.cpp + src/voice_audio_io.cpp src/audio_device.cpp src/connmonitorwindow.cpp transport/gui_bridge_impl.cpp + transport/miniaudio_impl.c transport/audio_diagnostics.cpp) +target_link_libraries(test_call_commands PRIVATE ${QT_LIBS} ${QT_TEST_LIB} chatgui_stream_publish utun_voice utun pthread ${CMAKE_DL_LIBS}) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_compile_definitions(test_call_commands PRIVATE UTUN_TEST_CALL_HOOKS) + foreach(symbol u_calloc_impl call_start_trampoline call_accept_trampoline call_decline_trampoline call_hangup_trampoline call_continue_trampoline) + target_link_options(test_call_commands PRIVATE "-Wl,--wrap=${symbol}") + endforeach() +endif() +add_test(NAME test_call_commands COMMAND test_call_commands) +set_tests_properties(test_call_commands PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30) + add_executable(test_member_dialogs tests/test_member_dialogs.cpp src/renamedialog.cpp src/memberpropsdialog.cpp db/db_manager.cpp transport/gui_bridge_impl.cpp src/connmonitorwindow.cpp) target_link_libraries(test_member_dialogs PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun utun_voice pthread ${CMAKE_DL_LIBS}) diff --git a/tools/chatgui/tests/test_call_commands.cpp b/tools/chatgui/tests/test_call_commands.cpp new file mode 100644 index 000000000..e3c544f91 --- /dev/null +++ b/tools/chatgui/tests/test_call_commands.cpp @@ -0,0 +1,222 @@ +/* Настоящие CallWindow/bridge/uasync: владение call-командами при отказе, отмене и исполнении. + * Linux hooks наблюдают границу native trampoline и затем вызывают его без подмены освобождения payload. */ +#include "../src/callwindow.h" +#include "../src/call_camera_capture.h" +#include "../transport/gui_bridge.h" +#include +#include +#include +#include +#include +#include +extern "C" { +#include "utun_instance.h" +#include "call/call.h" +#include "mem.h" +#include "debug_config.h" +} + +struct Observation { + UTUN_INSTANCE* instance; + uint64_t id, peer; + int action; + QByteArray channel; +}; +static QList observed; +static UTUN_INSTANCE* executingCore = nullptr; +static bool bindingValid = true; + +#ifdef UTUN_TEST_CALL_HOOKS +static std::atomic failAllocation{0}; +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_CALL, "[INJECT] call command allocation failure at %s", where); return nullptr; + } + return __real_u_calloc_impl(count, size, where); +} +extern "C" void __real_call_start_trampoline(void*); +extern "C" void __wrap_call_start_trampoline(void* arg) { + auto* request = static_cast(arg); + bindingValid &= request->inst == executingCore; + observed.append({request->inst, request->call_id, request->peer_node_id, -1, request->ch_id}); + __real_call_start_trampoline(arg); +} +static void observeAction(void* arg, int action, void (*native)(void*)) { + auto* request = static_cast(arg); + bindingValid &= request->inst == executingCore; + observed.append({request->inst, request->call_id, 0, action, {}}); native(arg); +} +extern "C" void __real_call_accept_trampoline(void*); +extern "C" void __wrap_call_accept_trampoline(void* arg) { observeAction(arg, GUI_CALL_ACCEPT, __real_call_accept_trampoline); } +extern "C" void __real_call_decline_trampoline(void*); +extern "C" void __wrap_call_decline_trampoline(void* arg) { observeAction(arg, GUI_CALL_DECLINE, __real_call_decline_trampoline); } +extern "C" void __real_call_hangup_trampoline(void*); +extern "C" void __wrap_call_hangup_trampoline(void* arg) { observeAction(arg, GUI_CALL_HANGUP, __real_call_hangup_trampoline); } +extern "C" void __real_call_continue_trampoline(void*); +extern "C" void __wrap_call_continue_trampoline(void* arg) { observeAction(arg, GUI_CALL_CONTINUE, __real_call_continue_trampoline); } +#endif + +struct CommandCore { + UASYNC* ua = uasync_create(); + UTUN_INSTANCE instance{}; + ~CommandCore() { + 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); + } + void poll() { executingCore = &instance; uasync_poll(ua, 0); executingCore = nullptr; } +}; +static int pending(UASYNC* ua) { + int count = 0; + for (auto* task = ua->posted_tasks_head; task; task = task->next) ++count; + return count; +} + +class CallCommandsTest : public QObject { + Q_OBJECT + static void scenarios() { + QTest::addColumn("scenario"); + for (const char* value : {"unavailable", "stopped", "cancelled", "executed"}) QTest::newRow(value) << QString(value); +#ifdef UTUN_TEST_CALL_HOOKS + QTest::newRow("request-allocation") << QString("request-allocation"); + QTest::newRow("post-allocation") << QString("post-allocation"); +#endif + } + static void prepare(CommandCore& core, const QString& scenario) { + core.attach(); + if (scenario == "unavailable") gui_bridge_set_inst(nullptr); + if (scenario == "stopped") uasync_mark_stopped(core.ua); +#ifdef UTUN_TEST_CALL_HOOKS + failAllocation = scenario == "request-allocation" ? 1 : scenario == "post-allocation" ? 2 : 0; +#endif + } +private slots: + void initTestCase() { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); + debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG); gui_bridge_init(); + { CallWindow warmup; } // Singleton audio engine создаётся до подсчёта командных аллокаций. + } + void cleanup() { +#ifdef UTUN_TEST_CALL_HOOKS + failAllocation = 0; +#endif + gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); + observed.clear(); bindingValid = true; + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + } + void windowAction_data() { + QTest::addColumn("scenario"); QTest::addColumn("action"); + QStringList failures{"unavailable", "stopped", "cancelled", "executed"}; +#ifdef UTUN_TEST_CALL_HOOKS + failures << "request-allocation" << "post-allocation"; +#endif + for (const auto& failure : failures) for (const char* action : {"onAccept", "onDecline", "onHangup", "continue", "close"}) + QTest::newRow(qPrintable(failure + '-' + action)) << failure << QString(action); + } + void windowAction() { + QFETCH(QString, scenario); QFETCH(QString, action); + const size_t before = u_get_allocated_count(); + { + CommandCore core; QVERIFY(core.ua); + CallWindow window; window.setCallId(0x123456789); + const auto state = action == "onAccept" || action == "onDecline" ? CallWindow::Incoming : + action == "continue" ? CallWindow::WaitDecision : CallWindow::Outgoing; + window.setState(state); prepare(core, scenario); + const size_t ready = u_get_allocated_count(); + if (action == "continue") { + QPushButton* button = nullptr; + for (auto* candidate : window.findChildren()) + if (candidate->text() == QCoreApplication::translate("ChatGui", "Call anyway")) button = candidate; + QVERIFY(button); button->click(); + } else if (action == "close") { window.show(); window.close(); } + else QVERIFY(QMetaObject::invokeMethod(&window, qPrintable(action), Qt::DirectConnection)); + const bool accepted = scenario == "executed" || scenario == "cancelled"; + QCOMPARE(pending(core.ua), accepted ? 1 : 0); + if (!accepted && action != "close") { + QCOMPARE(window.state(), state); + bool message = false; + for (auto* label : window.findChildren()) + message |= label->text() == QCoreApplication::translate("ChatGui", "Could not send the call command"); + QVERIFY(message); + } + if (scenario == "executed") core.poll(); + else gui_bridge_set_inst(nullptr); + QCOMPARE(pending(core.ua), 0); QCOMPARE(u_get_allocated_count(), ready); +#ifdef UTUN_TEST_CALL_HOOKS + QCOMPARE(failAllocation.load(), 0); QCOMPARE(observed.size(), scenario == "executed" ? 1 : 0); + if (!observed.isEmpty()) { + const int expected = action == "onAccept" ? GUI_CALL_ACCEPT : action == "onDecline" ? GUI_CALL_DECLINE : + action == "continue" ? GUI_CALL_CONTINUE : GUI_CALL_HANGUP; + QCOMPARE(observed.front().action, expected); QCOMPARE(observed.front().id, uint64_t(0x123456789)); + } +#endif + QVERIFY(bindingValid); + } + QCOMPARE(u_get_allocated_count(), before); + } + void startLifecycle_data() { scenarios(); } + void startLifecycle() { + QFETCH(QString, scenario); + const size_t before = u_get_allocated_count(); + { + CommandCore core; QVERIFY(core.ua); prepare(core, scenario); + const size_t ready = u_get_allocated_count(); + char channel[] = "dm:123456789abcdef0"; + const bool accepted = scenario == "executed" || scenario == "cancelled"; + QCOMPARE(gui_bridge_call_start(channel, 42, 73), accepted ? 0 : -1); + memset(channel, 'x', sizeof(channel) - 1); // После возврата caller может менять/освобождать строку. + if (scenario == "executed") core.poll(); + else gui_bridge_set_inst(nullptr); + QCOMPARE(pending(core.ua), 0); QCOMPARE(u_get_allocated_count(), ready); +#ifdef UTUN_TEST_CALL_HOOKS + QCOMPARE(failAllocation.load(), 0); QCOMPARE(observed.size(), scenario == "executed" ? 1 : 0); + if (!observed.isEmpty()) { + QCOMPARE(observed.front().channel, QByteArray("dm:123456789abcdef0")); + QCOMPARE(observed.front().peer, uint64_t(42)); QCOMPARE(observed.front().id, uint64_t(73)); + } +#endif + QVERIFY(bindingValid); + } + QCOMPARE(u_get_allocated_count(), before); + } + void invalidArguments() { + CommandCore core; QVERIFY(core.ua); core.attach(); const size_t before = u_get_allocated_count(); + char channel[64]; memset(channel, '1', sizeof(channel)); + QCOMPARE(gui_bridge_call_start(nullptr, 1, 1), -1); QCOMPARE(gui_bridge_call_start("", 1, 1), -1); + QCOMPARE(gui_bridge_call_start(channel, 1, 1), -1); QCOMPARE(gui_bridge_call_start("42", 0, 1), -1); + QCOMPARE(gui_bridge_call_start("42", 1, 0), -1); + QCOMPARE(gui_bridge_call_action(static_cast(99), 1), -1); + QCOMPARE(gui_bridge_call_action(GUI_CALL_ACCEPT, 0), -1); + QCOMPARE(pending(core.ua), 0); QCOMPARE(u_get_allocated_count(), before); + } + void concurrentRebind() { + const size_t before = u_get_allocated_count(); + { + CommandCore first, second; QVERIFY(first.ua); QVERIFY(second.ua); first.attach(); + std::thread producer([] { + for (uint64_t id = 1; id <= 64; ++id) gui_bridge_call_action(GUI_CALL_HANGUP, id); + }); + for (unsigned i = 0; i < 32; ++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_call_action(GUI_CALL_HANGUP, 1000), 0); first.poll(); + QVERIFY(bindingValid); QCOMPARE(pending(first.ua), 0); QCOMPARE(pending(second.ua), 0); +#ifdef UTUN_TEST_CALL_HOOKS + QVERIFY(!observed.isEmpty()); QCOMPARE(observed.back().instance, &first.instance); +#endif + } + QCOMPARE(u_get_allocated_count(), before); + } +}; + +int main(int argc, char** argv) { + if (argc >= 2 && !strcmp(argv[1], "--call-camera-worker")) return call_camera_worker_main(argc, argv); + QApplication application(argc, argv); CallCommandsTest test; return QTest::qExec(&test, argc, argv); +} +#include "test_call_commands.moc"