From 271cfcd2f8c81b8f317f25a72b722d5d26dfe407 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 11:17:47 +0100 Subject: [PATCH] Cover channel settings queue failures and nested dialog lifetime --- tools/chatgui/CMakeLists.txt | 10 + tools/chatgui/tests/test_channel_settings.cpp | 172 ++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 tools/chatgui/tests/test_channel_settings.cpp diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index a93101be..103ccb3e 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -480,6 +480,16 @@ target_link_libraries(test_channel_list PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun u add_test(NAME test_channel_list COMMAND test_channel_list) set_tests_properties(test_channel_list PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30) +add_executable(test_channel_settings tests/test_channel_settings.cpp src/channelsettingsdialog.cpp + src/avatars/groupavatarmanager.cpp db/db_manager.cpp transport/gui_bridge_impl.cpp) +target_link_libraries(test_channel_settings PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun utun_voice pthread ${CMAKE_DL_LIBS}) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_compile_definitions(test_channel_settings PRIVATE UTUN_TEST_SETTINGS_ALLOC_FAILURES) + target_link_options(test_channel_settings PRIVATE "-Wl,--wrap=u_malloc_impl" "-Wl,--wrap=u_calloc_impl") +endif() +add_test(NAME test_channel_settings COMMAND test_channel_settings) +set_tests_properties(test_channel_settings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30) + add_executable(test_single_instance tests/test_single_instance.cpp src/single_instance.cpp) target_link_libraries(test_single_instance PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun pthread ${CMAKE_DL_LIBS}) if(WIN32) diff --git a/tools/chatgui/tests/test_channel_settings.cpp b/tools/chatgui/tests/test_channel_settings.cpp new file mode 100644 index 00000000..8467ed73 --- /dev/null +++ b/tools/chatgui/tests/test_channel_settings.cpp @@ -0,0 +1,172 @@ +/* Настоящие Qt-диалоги/SQLite/bridge: отказы очереди и владение вложенными окнами. */ +#include "../src/channelsettingsdialog.h" +#include "../src/avatars/groupavatarmanager.h" +#include "../db/db_manager.h" +#include "../transport/gui_bridge.h" +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include "utun_instance.h" +#include "chat/chat_core.h" +#include "mem.h" +#include "debug_config.h" +} + +#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES +static int failAllocation; +static const char* allocationFile; +static bool rejectAllocation(const char* where) { + if (!failAllocation || !strstr(where, allocationFile) || --failAllocation) return false; + DEBUG_WARN(DEBUG_CATEGORY_CHAT, "[INJECT] channel settings allocation failure at %s", where); return true; +} +extern "C" void* __real_u_malloc_impl(uint32_t, const char*); +extern "C" void* __wrap_u_malloc_impl(uint32_t bytes, const char* where) { + return rejectAllocation(where) ? nullptr : __real_u_malloc_impl(bytes, where); +} +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* where) { + return rejectAllocation(where) ? nullptr : __real_u_calloc_impl(count, bytes, where); +} +#endif + +struct SettingsCore { + UASYNC* ua = uasync_create(); + UTUN_INSTANCE* inst = nullptr; + DbManager db; + QString channel; + ~SettingsCore() { + gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); + if (ua) uasync_mark_stopped(ua); + db.setDb(nullptr); + if (inst) utun_instance_destroy(inst); + if (ua) uasync_destroy(ua, 0); + } + bool start(bool attach = true) { + if (!ua) return false; + const char* config = "[global]\n" + "my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" + "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" + "[chatserver]\ngroup_autoconnect=0\nmember_files_enabled=0\nstorage_autoload=0\n"; + inst = utun_instance_create_from_str(ua, config); + if (!inst || utun_core_start(inst) || chat_service_start(inst)) return false; + chat_core_create_channel_auto(inst, "Settings"); + db.setDb(inst->topo_sqlite_db); db.setMyNodeId(inst->node_id); + if (db.getChannels().size() != 1) return false; + channel = db.getChannels()[0].channelId; + if (attach) { gui_bridge_set_uasync(ua); gui_bridge_set_inst(inst); } + return true; + } +}; + +class ChannelSettingsTest : public QObject { + Q_OBJECT +private slots: + void initTestCase() { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); gui_bridge_init(); + QApplication::setAttribute(Qt::AA_DontUseNativeDialogs); + } + + void pendingSaveIsOwned() { + SettingsCore core; QVERIFY(core.start()); + ChannelSettingsDialog dialog(core.channel, &core.db); + const auto baseline = u_get_allocated_count(); + QSignalSpy accepted(&dialog, &QDialog::accepted); + QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); + QCOMPARE(accepted.count(), 1); + gui_bridge_set_inst(nullptr); + QCOMPARE(u_get_allocated_count(), baseline); + } + + void savesBothValues() { + SettingsCore core; QVERIFY(core.start()); + ChannelSettingsDialog dialog(core.channel, &core.db); + const auto checks = dialog.findChildren(); QCOMPARE(checks.size(), 2); + QVERIFY(!checks[0]->isChecked()); QVERIFY(checks[1]->isChecked()); + checks[0]->setChecked(true); checks[1]->setChecked(false); + QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); + uasync_poll(core.ua, 0); + QCOMPARE(core.db.getUiStateInt("ch_" + core.channel + "_auto_play_voice", -1), 1); + QCOMPARE(core.db.getUiStateInt("ch_" + core.channel + "_sound_on_message", -1), 0); + } + + void stoppedCoreKeepsDialogOpen() { + SettingsCore core; QVERIFY(core.start(false)); + ChannelSettingsDialog dialog(core.channel, &core.db); + QSignalSpy accepted(&dialog, &QDialog::accepted); + QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); + QVERIFY(accepted.isEmpty()); + auto* status = dialog.findChild("channelSettingsStatus"); QVERIFY(status && !status->text().isEmpty()); + } + +#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES + void allocationFailure_data() { + QTest::addColumn("source"); QTest::addColumn("position"); + for (const auto* source : {"channelsettingsdialog.cpp", "gui_bridge_impl.cpp"}) + for (int position : {1, 2}) QTest::newRow(qPrintable(QString("%1-%2").arg(source).arg(position))) << QByteArray(source) << position; + } + void allocationFailure() { + QFETCH(QByteArray, source); QFETCH(int, position); + SettingsCore core; QVERIFY(core.start()); + ChannelSettingsDialog dialog(core.channel, &core.db); + QSignalSpy accepted(&dialog, &QDialog::accepted); + const auto baseline = u_get_allocated_count(); + allocationFile = source.constData(); failAllocation = position; + auto reset = qScopeGuard([] { failAllocation = 0; allocationFile = nullptr; }); + QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); + QCOMPARE(failAllocation, 0); QVERIFY(accepted.isEmpty()); + gui_bridge_set_inst(nullptr); QCOMPARE(u_get_allocated_count(), baseline); + auto* status = dialog.findChild("channelSettingsStatus"); QVERIFY(status && !status->text().isEmpty()); + } +#endif + + void invalidChannel_data() { + QTest::addColumn("channel"); + for (const auto* value : {"", "0", "01", "+1", "1 ", "dm:1", "18446744073709551616"}) + QTest::newRow(value) << QString::fromLatin1(value); + QTest::newRow("embedded-nul") << QString::fromLatin1("1\0tail", 6); + } + void invalidChannel() { + QFETCH(QString, channel); + SettingsCore core; QVERIFY(core.start()); + ChannelSettingsDialog dialog(channel, &core.db); + QSignalSpy accepted(&dialog, &QDialog::accepted); + QVERIFY(!dialog.findChild("deleteChannel")->isEnabled()); + QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); + QVERIFY(accepted.isEmpty()); + } + + void borrowedDbDestruction() { + SettingsCore core; QVERIFY(core.start()); + auto* db = new DbManager; db->setDb(core.inst->topo_sqlite_db); + ChannelSettingsDialog dialog(core.channel, db); + QSignalSpy rejected(&dialog, &QDialog::rejected); delete db; + QCOMPARE(rejected.count(), 1); + QSignalSpy accepted(&dialog, &QDialog::accepted); + QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); QVERIFY(accepted.isEmpty()); + } + + void confirmationSurvivesOwnerDeletion() { + SettingsCore core; QVERIFY(core.start()); + QPointer dialog = new ChannelSettingsDialog(core.channel, &core.db); + dialog->show(); + QTimer::singleShot(0, [&] { QVERIFY(qobject_cast(QApplication::activeModalWidget())); delete dialog.data(); }); + QVERIFY(QMetaObject::invokeMethod(dialog.data(), "onDelete", Qt::DirectConnection)); + QVERIFY(!dialog); QCOMPARE(core.db.getChannels().size(), 1); + } + + void avatarChooserSurvivesOwnerDeletion() { + SettingsCore core; QVERIFY(core.start()); GroupAvatarManager avatars(&core.db); + QPointer dialog = new ChannelSettingsDialog(core.channel, &core.db); + dialog->show(); auto* upload = dialog->findChild("uploadGroupAvatar"); QVERIFY(upload && upload->isEnabled()); + QTimer::singleShot(0, [&] { QVERIFY(qobject_cast(QApplication::activeModalWidget())); delete dialog.data(); }); + upload->click(); QVERIFY(!dialog); + } +}; + +QTEST_MAIN(ChannelSettingsTest) +#include "test_channel_settings.moc"