From 9ee64d9a9fdee8932bbb06a539806ede213d2196 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 00:37:33 +0100 Subject: [PATCH] Extend desktop command regression suite and verify SQLite stopping handoff --- tools/chatgui/CMakeLists.txt | 22 +++++------ ...commands.cpp => test_desktop_commands.cpp} | 38 +++++++++---------- tools/chatgui/tests/test_utun_node.cpp | 23 ++++++++++- 3 files changed, 51 insertions(+), 32 deletions(-) rename tools/chatgui/tests/{test_radio_commands.cpp => test_desktop_commands.cpp} (92%) diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 494939022..78df2fcd9 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -580,19 +580,19 @@ 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_compile_definitions(test_radio_commands PRIVATE $) -target_link_libraries(test_radio_commands PRIVATE ${DM_DELETION_LIBRARIES} ${QT_TEST_LIB}) +add_executable(test_desktop_commands tests/test_desktop_commands.cpp ${DM_DELETION_SOURCES}) +add_dependencies(test_desktop_commands chatgui_translations) +target_include_directories(test_desktop_commands PRIVATE $) +target_compile_definitions(test_desktop_commands PRIVATE $) +target_link_libraries(test_desktop_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}") + target_compile_definitions(test_desktop_commands PRIVATE UTUN_TEST_DESKTOP_HOOKS) + foreach(symbol u_malloc_impl u_calloc_impl u_free_impl radio_call_trampoline ma_context_init sqlite3_step sqlite3_bind_text sqlite3_finalize) + target_link_options(test_desktop_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_test(NAME test_desktop_commands COMMAND test_desktop_commands) +set_tests_properties(test_desktop_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) @@ -982,7 +982,7 @@ add_test(NAME test_voice_file COMMAND test_voice_file) if(WIN32) # AudioDevice закрывает неисправный WASAPI в worker с собственным COM apartment. foreach(audio_target vibechat test_file_drop test_invite_links test_video_engine test_stream_view test_stream_publish test_call_commands - test_voice_file test_sound_settings test_settings_save test_radio_commands) + test_voice_file test_sound_settings test_settings_save test_desktop_commands) target_link_libraries(${audio_target} PRIVATE ole32) endforeach() endif() diff --git a/tools/chatgui/tests/test_radio_commands.cpp b/tools/chatgui/tests/test_desktop_commands.cpp similarity index 92% rename from tools/chatgui/tests/test_radio_commands.cpp rename to tools/chatgui/tests/test_desktop_commands.cpp index 7303f79f5..af4d4e28a 100644 --- a/tools/chatgui/tests/test_radio_commands.cpp +++ b/tools/chatgui/tests/test_desktop_commands.cpp @@ -1,4 +1,4 @@ -/* Настоящие MainWindow/bridge/radio/uasync: отказ, отмена, OOM и привязка команды к владельцу очереди. +/* Desktop MainWindow/MessageList/delegate/SQLite/bridge: отказы, отмена, OOM и привязка к владельцу. * Native trampoline исполняется без подмены; пустой каталог CHAT даёт штатный FINISHED/UNAVAILABLE для START. */ #include "../src/mainwindow.h" #include "../transport/gui_bridge.h" @@ -24,7 +24,7 @@ static UTUN_INSTANCE* executingCore = nullptr; static bool bindingValid = true; static QList observed; -#ifdef UTUN_TEST_RADIO_HOOKS +#ifdef UTUN_TEST_DESKTOP_HOOKS #include "miniaudio.h" static std::atomic watching{false}; static std::atomic failAllocation{0}; @@ -69,13 +69,13 @@ extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const } #endif -struct RadioCore { +struct DesktopCore { QTemporaryDir directory; UASYNC* ua = uasync_create(); UTUN_INSTANCE* inst = nullptr; unsigned results = 0; bool resultValid = true; - ~RadioCore() { + ~DesktopCore() { gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); if (ua) uasync_mark_stopped(ua); if (inst) utun_instance_destroy(inst); @@ -98,7 +98,7 @@ struct RadioCore { 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(instance->test_user_ptr); ++core->results; + auto* core = static_cast(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; @@ -112,7 +112,7 @@ static unsigned pending(UASYNC* ua) { return count; } -class RadioCommandsTest : public QObject { +class DesktopCommandsTest : public QObject { Q_OBJECT private slots: void initTestCase() { @@ -122,17 +122,17 @@ private slots: } void init() { observed.clear(); bindingValid = true; -#ifdef UTUN_TEST_RADIO_HOOKS +#ifdef UTUN_TEST_DESKTOP_HOOKS watching = false; failAllocation = 0; #endif } void cleanup() { -#ifdef UTUN_TEST_RADIO_HOOKS +#ifdef UTUN_TEST_DESKTOP_HOOKS failAllocation = 0; #endif gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); -#ifdef UTUN_TEST_RADIO_HOOKS +#ifdef UTUN_TEST_DESKTOP_HOOKS watching = false; std::lock_guard lock(allocationMutex); for (const auto& item : outstanding) @@ -143,7 +143,7 @@ private slots: void lifecycle_data() { QTest::addColumn("action"); QTest::addColumn("scenario"); QTest::addColumn("window"); QStringList scenarios{"unavailable", "stopped", "cancelled", "executed"}; -#ifdef UTUN_TEST_RADIO_HOOKS +#ifdef UTUN_TEST_DESKTOP_HOOKS scenarios << "request-oom" << "queue-oom"; #endif for (int action = GUI_RADIO_CALL_START; action <= GUI_RADIO_CALL_CONTINUE; ++action) @@ -155,12 +155,12 @@ private slots: } void lifecycle() { QFETCH(int, action); QFETCH(QString, scenario); QFETCH(bool, window); - RadioCore core; QVERIFY(core.start()); + DesktopCore core; QVERIFY(core.start()); std::unique_ptr form; if (window) form = std::make_unique(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 +#ifdef UTUN_TEST_DESKTOP_HOOKS watching = true; failAllocation = scenario == "request-oom" ? 1 : scenario == "queue-oom" ? 2 : 0; #endif const bool accepted = scenario == "executed" || scenario == "cancelled" || scenario == "destroyed"; @@ -174,7 +174,7 @@ private slots: 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 +#ifdef UTUN_TEST_DESKTOP_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, @@ -189,7 +189,7 @@ private slots: window, action, qPrintable(scenario), core.results); } void invalidArguments() { - RadioCore core; QVERIFY(core.start()); const auto baseline = u_get_allocated_count(); + DesktopCore core; QVERIFY(core.start()); const auto baseline = u_get_allocated_count(); QCOMPARE(gui_bridge_radio_call(static_cast(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); @@ -200,8 +200,8 @@ private slots: 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 + DesktopCore first, second; QVERIFY(first.start()); gui_bridge_set_inst(nullptr); QVERIFY(second.start()); +#ifdef UTUN_TEST_DESKTOP_HOOKS watching = true; #endif std::atomic turn{0}, submitted{0}; @@ -224,7 +224,7 @@ private slots: QCOMPARE(gui_bridge_radio_call(GUI_RADIO_CALL_START, 2000, 7654321, 42), 0); first.poll(); QVERIFY(controlsAccepted); 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 +#ifdef UTUN_TEST_DESKTOP_HOOKS size_t firstCount = 0, secondCount = 0; for (const auto& request : observed) { firstCount += request.inst == first.inst; secondCount += request.inst == second.inst; } QVERIFY(firstCount >= 49); QVERIFY(secondCount >= 48); @@ -235,5 +235,5 @@ private slots: } }; -QTEST_MAIN(RadioCommandsTest) -#include "test_radio_commands.moc" +QTEST_MAIN(DesktopCommandsTest) +#include "test_desktop_commands.moc" diff --git a/tools/chatgui/tests/test_utun_node.cpp b/tools/chatgui/tests/test_utun_node.cpp index 45c31b307..6f60463c1 100644 --- a/tools/chatgui/tests/test_utun_node.cpp +++ b/tools/chatgui/tests/test_utun_node.cpp @@ -115,7 +115,12 @@ private slots: } } + void coreStopFinishesOwnerThread_data() { + QTest::addColumn("direct"); + QTest::newRow("core-stop") << false; QTest::newRow("GUI-stop") << true; + } void coreStopFinishesOwnerThread() { + QFETCH(bool, direct); QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("node.cfg"); QFile config(path); QVERIFY(config.open(QIODevice::WriteOnly)); @@ -123,10 +128,24 @@ private slots: QCOMPARE(config.write(text), text.size()); config.close(); UtunNode node; node.setDbPath(directory.filePath("db")); node.setDebugFile(directory.filePath("node.log")); - QSignalSpy stopped(&node, &UtunNode::stopped); + QSignalSpy stopped(&node, &UtunNode::stopped), stopping(&node, &UtunNode::stopping); + sqlite3* borrowed = nullptr; bool liveDuringDetach = false; + connect(&node, &UtunNode::stopping, &node, [&] { + sqlite3_stmt* statement = nullptr; + if (borrowed && QThread::currentThread() == qApp->thread()) { + int rc = sqlite3_prepare_v2(borrowed, "SELECT node_id FROM local_identity WHERE id=1", -1, &statement, nullptr); + if (rc == SQLITE_OK) rc = sqlite3_step(statement); + const int final = sqlite3_finalize(statement); liveDuringDetach = rc == SQLITE_ROW && final == SQLITE_OK; + } + borrowed = nullptr; + }); QVERIFY(node.start(path)); - gui_bridge_post_uasync_fn([](void*) { utun_instance_stop(gui_bridge_get_inst()); }, nullptr); + borrowed = node.databaseHandle(); QVERIFY(borrowed); + QCOMPARE(node.databasePath(), directory.filePath("db")); + if (direct) node.stop(); + else gui_bridge_post_uasync_fn([](void*) { utun_instance_stop(gui_bridge_get_inst()); }, nullptr); QTRY_COMPARE_WITH_TIMEOUT(stopped.count(), 1, 3000); + QCOMPARE(stopping.count(), 1); QVERIFY(liveDuringDetach); QVERIFY(!borrowed); QVERIFY(!node.databaseHandle()); node.stop(); QVERIFY(!node.isRunning() && !node.instance() && !gui_bridge_get_inst() && !gui_bridge_is_uasync_ready()); }