Extend desktop command regression suite and verify SQLite stopping handoff

This commit is contained in:
evgeny committed 2026-10-28 00:59:00 +01:00
1 parent 72f4258410
commit 9ee64d9a9f
3 files changed
+51 -32

No files matched your search

+11 -11
View File
@@ -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_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})
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_PROPERTY:vibechat,INCLUDE_DIRECTORIES>)
target_compile_definitions(test_desktop_commands PRIVATE $<TARGET_PROPERTY:vibechat,COMPILE_DEFINITIONS>)
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()
@@ -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<radio_call_request> observed;
#ifdef UTUN_TEST_RADIO_HOOKS
#ifdef UTUN_TEST_DESKTOP_HOOKS
#include "miniaudio.h"
static std::atomic<bool> watching{false};
static std::atomic<int> 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<RadioCore*>(instance->test_user_ptr); ++core->results;
auto* core = static_cast<DesktopCore*>(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<std::mutex> lock(allocationMutex);
for (const auto& item : outstanding)
@@ -143,7 +143,7 @@ private slots:
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
#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<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
#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<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);
@@ -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<unsigned> 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"
+21 -2
View File
@@ -115,7 +115,12 @@ private slots:
}
}
void coreStopFinishesOwnerThread_data() {
QTest::addColumn<bool>("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());
}