From 821f1b8a9214caa6d43ac11eceb2935ca7c32533 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 15:11:22 +0100 Subject: [PATCH] Add sound settings request ownership regressions --- tools/chatgui/CMakeLists.txt | 14 +- tools/chatgui/tests/test_sound_settings.cpp | 174 ++++++++++++++++++++ 2 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 tools/chatgui/tests/test_sound_settings.cpp diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index d227db63..5abe92f2 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -490,6 +490,17 @@ 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_sound_settings tests/test_sound_settings.cpp src/soundsettingspage.cpp src/sound_manager.cpp + src/audio_device.cpp src/voice_audio_io.cpp db/db_manager.cpp transport/gui_bridge_impl.cpp + transport/miniaudio_impl.c transport/audio_diagnostics.cpp) +target_link_libraries(test_sound_settings PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun_voice utun pthread ${CMAKE_DL_LIBS}) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_compile_definitions(test_sound_settings PRIVATE UTUN_TEST_SETTINGS_ALLOC_FAILURES) + target_link_options(test_sound_settings PRIVATE "-Wl,--wrap=u_malloc_impl" "-Wl,--wrap=u_calloc_impl") +endif() +add_test(NAME test_sound_settings COMMAND test_sound_settings) +set_tests_properties(test_sound_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) @@ -872,7 +883,8 @@ 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_voice_file) + foreach(audio_target vibechat test_file_drop test_invite_links test_video_engine test_stream_view test_stream_publish + test_voice_file test_sound_settings) target_link_libraries(${audio_target} PRIVATE ole32) endforeach() endif() diff --git a/tools/chatgui/tests/test_sound_settings.cpp b/tools/chatgui/tests/test_sound_settings.cpp new file mode 100644 index 00000000..70b5b702 --- /dev/null +++ b/tools/chatgui/tests/test_sound_settings.cpp @@ -0,0 +1,174 @@ +/* Настоящие SoundSettingsPage/bridge/SQLite: выполнение, отказ и отмена сохранения без утечек. */ +#include "../src/soundsettingspage.h" +#include "../src/sound_manager.h" +#include "../db/db_manager.h" +#include "../transport/gui_bridge.h" +#include +#include +#include +#include +#include +extern "C" { +#include "utun_instance.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_CONFIG, "[INJECT] sound 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 SoundSettingsCore { + UASYNC* ua = uasync_create(); + UTUN_INSTANCE* inst = nullptr; + DbManager db; + ~SoundSettingsCore() { + 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() { + 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; + db.setDb(inst->topo_sqlite_db); + gui_bridge_set_uasync(ua); gui_bridge_set_inst(inst); + uasync_poll(ua, 0); + return true; + } +}; + +static const char* events[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect", + "call_incoming", "call_connecting", "call_ringing", "radio_incoming"}; + +static QMap expectedSettings(int revision) { + QMap values{{"sound_enabled", QString::number(revision % 2)}, + {"sound_volume", QString::number(37 + revision)}}; + for (int i = 0; i < 8; ++i) { + values.insert(QString("sound_%1_on").arg(events[i]), QString::number((i + revision) % 2)); + values.insert(QString("sound_%1_vol").arg(events[i]), QString::number((i * 17 + revision) % 101)); + } + return values; +} + +class SoundSettingsTest : public QObject { + Q_OBJECT + void editPage(SoundSettingsPage& page, const QMap& values) { + const auto checks = page.findChildren(); QCOMPARE(checks.size(), 9); + const auto sliders = page.findChildren(); QCOMPARE(sliders.size(), 9); + checks[0]->setChecked(values["sound_enabled"].toInt()); sliders[0]->setValue(values["sound_volume"].toInt()); + for (int i = 0; i < 8; ++i) { + checks[i + 1]->setChecked(values[QString("sound_%1_on").arg(events[i])].toInt()); + sliders[i + 1]->setValue(values[QString("sound_%1_vol").arg(events[i])].toInt()); + } + } + void checkLocalSettings(const QMap& values) { + auto* manager = SoundManager::instance(); + QCOMPARE(manager->isEnabled(), bool(values["sound_enabled"].toInt())); + QCOMPARE(manager->volume(), values["sound_volume"].toInt() / 100.0f); + for (const auto* event : events) { + const auto settings = manager->eventSettings(event); + QCOMPARE(settings.enabled, bool(values[QString("sound_%1_on").arg(event)].toInt())); + QCOMPARE(settings.volume, values[QString("sound_%1_vol").arg(event)].toInt() / 100.0f); + } + } +private slots: + void initTestCase() { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); + debug_set_category_level(DEBUG_CATEGORY_CONFIG, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_CHAT, DEBUG_LEVEL_DEBUG); + gui_bridge_init(); SoundManager::instance(); // Настройки не требуют открытия звукового устройства. + } + void unavailableCoreReleasesRequests() { + SoundSettingsPage page(nullptr); + const auto baseline = u_get_allocated_count(); + for (int revision : {1, 2}) { + const auto values = expectedSettings(revision); editPage(page, values); + page.applyAndSave(); checkLocalSettings(values); + QCOMPARE(u_get_allocated_count(), baseline); + } + } + void detachingCoreReleasesPendingRequests() { + SoundSettingsCore core; QVERIFY(core.start()); + SoundSettingsPage page(&core.db); + const auto values = expectedSettings(1); editPage(page, values); + const auto baseline = u_get_allocated_count(); + page.applyAndSave(); checkLocalSettings(values); + QVERIFY(core.ua->posted_tasks_head); + for (auto it = values.cbegin(); it != values.cend(); ++it) QVERIFY(core.db.getUiState(it.key()).isEmpty()); + gui_bridge_set_inst(nullptr); + QCOMPARE(u_get_allocated_count(), baseline); + uasync_poll(core.ua, 0); + for (auto it = values.cbegin(); it != values.cend(); ++it) QVERIFY(core.db.getUiState(it.key()).isEmpty()); + } + void savesSnapshotAfterPageDestruction() { + SoundSettingsCore core; QVERIFY(core.start()); + const auto baseline = u_get_allocated_count(); + for (int revision : {1, 2}) { + const auto values = expectedSettings(revision); + { + SoundSettingsPage page(&core.db); editPage(page, values); + page.applyAndSave(); checkLocalSettings(values); + } + // Очередь хранит снимок значений независимо от времени жизни формы. + QVERIFY(core.ua->posted_tasks_head); + uasync_poll(core.ua, 0); + for (auto it = values.cbegin(); it != values.cend(); ++it) QCOMPARE(core.db.getUiState(it.key()), it.value()); + QCOMPARE(u_get_allocated_count(), baseline); + } + } +#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES + void allocationFailure_data() { + QTest::addColumn("source"); QTest::addColumn("position"); QTest::addColumn("cancel"); + for (const auto* source : {"soundsettingspage.cpp", "gui_bridge_impl.cpp"}) + for (int position : {1, 9, 18}) + for (bool cancel : {false, true}) + QTest::newRow(qPrintable(QString("%1-%2-%3").arg(source).arg(position).arg(cancel ? "cancel" : "execute"))) + << QByteArray(source) << position << cancel; + } + void allocationFailure() { + QFETCH(QByteArray, source); QFETCH(int, position); QFETCH(bool, cancel); + SoundSettingsCore core; QVERIFY(core.start()); + SoundSettingsPage page(&core.db); + const auto values = expectedSettings(1); editPage(page, values); + const auto baseline = u_get_allocated_count(); + allocationFile = source.constData(); failAllocation = position; + auto reset = qScopeGuard([] { failAllocation = 0; allocationFile = nullptr; }); + page.applyAndSave(); QCOMPARE(failAllocation, 0); checkLocalSettings(values); + if (cancel) gui_bridge_set_inst(nullptr); + else uasync_poll(core.ua, 0); + QCOMPARE(u_get_allocated_count(), baseline); + int saved = 0; + for (auto it = values.cbegin(); it != values.cend(); ++it) { + const auto stored = core.db.getUiState(it.key()); + if (stored.isEmpty()) continue; + QCOMPARE(stored, it.value()); ++saved; + } + QCOMPARE(saved, cancel ? 0 : 17); + } +#endif +}; + +QTEST_MAIN(SoundSettingsTest) +#include "test_sound_settings.moc"