diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index b4bfdf41..8be62a5a 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -586,6 +586,12 @@ target_link_libraries(test_channel_replies PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utu add_test(NAME test_channel_replies COMMAND test_channel_replies) set_tests_properties(test_channel_replies PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30) +add_executable(test_database_maintenance tests/test_database_maintenance.cpp db/db_manager.cpp src/databasesettingspage.cpp) +target_include_directories(test_database_maintenance PRIVATE ${UTUN_INCLUDE_DIRS}) +target_link_libraries(test_database_maintenance PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun utun_voice pthread ${CMAKE_DL_LIBS}) +add_test(NAME test_database_maintenance COMMAND test_database_maintenance) +set_tests_properties(test_database_maintenance PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 15) + add_executable(test_member_files_browser tests/test_member_files_browser.cpp src/member_files/browser.cpp src/member_files/batch.cpp db/db_manager.cpp src/channellist.cpp src/channeldelegate.cpp src/avatars/groupavatarmanager.cpp) target_link_libraries(test_member_files_browser PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun pthread) diff --git a/tools/chatgui/tests/test_database_maintenance.cpp b/tools/chatgui/tests/test_database_maintenance.cpp new file mode 100644 index 00000000..5f8d8dde --- /dev/null +++ b/tools/chatgui/tests/test_database_maintenance.cpp @@ -0,0 +1,135 @@ +/* Настоящие SQLite/DbManager/Qt-страница: optimize без строк, строгие integrity-проверки и SQL-отказы. */ +#include "../db/db_manager.h" +#include "../src/databasesettingspage.h" +#include +#include +extern "C" { +#include "debug_config.h" +} + +using Operation = IntegrityResult (DbManager::*)() const; +static const Operation operations[] = {&DbManager::integrityCheckQuick, &DbManager::integrityCheckFull, &DbManager::integrityOptimize}; +static const char* titles[] = {"Quick Check", "Full Check", "Optimize"}; +static const char* slots[] = {"onQuickCheck", "onFullCheck", "onOptimize"}; + +struct Database { + QTemporaryDir directory; + sqlite3* handle = nullptr; + DbManager manager; + ~Database() { + manager.setDb(nullptr); + if (handle && sqlite3_close(handle) != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "test DB close failed: statements remain"); + } + bool execute(const char* sql) { + char* error = nullptr; + const int rc = sqlite3_exec(handle, sql, nullptr, nullptr, &error); + if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "test SQL failed rc=%d: %s", rc, error ? error : sqlite3_errmsg(handle)); + sqlite3_free(error); + return rc == SQLITE_OK; + } + bool open(bool disk = false) { + if (!directory.isValid()) return false; + const QByteArray path = disk ? directory.filePath("test.db").toUtf8() : QByteArray(":memory:"); + const int rc = sqlite3_open(path.constData(), &handle); + if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "test DB open failed rc=%d", rc); return false; } + manager.setDb(handle); + return execute("CREATE TABLE items(value INTEGER CHECK(value>0)); CREATE INDEX item_value ON items(value);" + "INSERT INTO items VALUES(1),(2),(3); SELECT value FROM items WHERE value=2;"); + } +}; + +static int denyPragma(void*, int action, const char*, const char*, const char*, const char*) { + return action == SQLITE_PRAGMA ? SQLITE_DENY : SQLITE_OK; +} + +static QString statusText(DatabaseSettingsPage& page) { + for (auto* label : page.findChildren()) + for (const auto* title : titles) if (label->text().startsWith(QString::fromLatin1(title) + ':')) return label->text(); + return {}; +} + +class DatabaseMaintenanceTest : public QObject { + Q_OBJECT +private slots: + void initTestCase() { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); + debug_set_category_level(DEBUG_CATEGORY_CONFIG, DEBUG_LEVEL_DEBUG); + } + void healthyOperations_data() { + QTest::addColumn("operation"); QTest::addColumn("disk"); + for (int operation = 0; operation < 3; ++operation) + for (bool disk : {false, true}) + QTest::newRow(qPrintable(QString("%1-%2").arg(titles[operation]).arg(disk ? "file" : "memory"))) << operation << disk; + } + void healthyOperations() { + QFETCH(int, operation); QFETCH(bool, disk); + Database db; QVERIFY(db.open(disk)); + for (int repeat = 0; repeat < 2; ++repeat) { + const auto result = (db.manager.*operations[operation])(); + QVERIFY2(result.ok, qPrintable(result.message)); + QCOMPARE(result.message, QString("ok")); QVERIFY(result.errors.isEmpty()); QVERIFY(result.elapsedMs >= 0); + QVERIFY(!sqlite3_next_stmt(db.handle, nullptr)); + } + } + void missingDatabase_data() { + QTest::addColumn("operation"); + for (int operation = 0; operation < 3; ++operation) QTest::newRow(titles[operation]) << operation; + } + void missingDatabase() { + QFETCH(int, operation); + DbManager manager; + const auto result = (manager.*operations[operation])(); + QVERIFY(!result.ok); QVERIFY(!result.message.isEmpty()); QCOMPARE(result.elapsedMs, 0); + } + void sqlFailure_data() { + QTest::addColumn("operation"); + for (int operation = 0; operation < 3; ++operation) QTest::newRow(titles[operation]) << operation; + } + void sqlFailure() { + QFETCH(int, operation); + Database db; QVERIFY(db.open()); + QCOMPARE(sqlite3_set_authorizer(db.handle, denyPragma, nullptr), SQLITE_OK); + const auto result = (db.manager.*operations[operation])(); + QVERIFY(!result.ok); QVERIFY2(result.message.contains("authorized"), qPrintable(result.message)); + QVERIFY(!sqlite3_next_stmt(db.handle, nullptr)); + QCOMPARE(sqlite3_set_authorizer(db.handle, nullptr, nullptr), SQLITE_OK); + const auto retry = (db.manager.*operations[operation])(); + QVERIFY2(retry.ok, qPrintable(retry.message)); + } + void integrityViolation_data() { + QTest::addColumn("operation"); + QTest::newRow("quick") << 0; QTest::newRow("full") << 1; + } + void integrityViolation() { + QFETCH(int, operation); + Database db; QVERIFY(db.open()); + QVERIFY(db.execute("PRAGMA ignore_check_constraints=ON; INSERT INTO items VALUES(-1),(-2); PRAGMA ignore_check_constraints=OFF;")); + const auto result = (db.manager.*operations[operation])(); + QVERIFY(!result.ok); QVERIFY2(result.message.contains("CHECK constraint failed"), qPrintable(result.message)); + QVERIFY(!result.errors.isEmpty()); QVERIFY(result.errors.first().contains("CHECK constraint failed")); + } + void pageShowsResult_data() { + QTest::addColumn("operation"); QTest::addColumn("denied"); + for (int operation = 0; operation < 3; ++operation) + for (bool denied : {false, true}) + QTest::newRow(qPrintable(QString("%1-%2").arg(titles[operation]).arg(denied ? "denied" : "success"))) << operation << denied; + } + void pageShowsResult() { + QFETCH(int, operation); QFETCH(bool, denied); + Database db; QVERIFY(db.open()); + DatabaseSettingsPage page(&db.manager); + if (denied) QCOMPARE(sqlite3_set_authorizer(db.handle, denyPragma, nullptr), SQLITE_OK); + QVERIFY(QMetaObject::invokeMethod(&page, slots[operation], Qt::DirectConnection)); + const auto buttons = page.findChildren(); QCOMPARE(buttons.size(), 3); + for (auto* button : buttons) QVERIFY(!button->isEnabled()); + QTRY_VERIFY_WITH_TIMEOUT(!statusText(page).isEmpty(), 1000); + QVERIFY2(statusText(page).contains(denied ? ": FAILED" : ": OK"), qPrintable(statusText(page))); + const auto details = page.findChild()->toPlainText(); + if (denied) QVERIFY2(details.contains("authorized"), qPrintable(details)); + else QCOMPARE(details, QString("ok")); + for (auto* button : buttons) QVERIFY(button->isEnabled()); + } +}; + +QTEST_MAIN(DatabaseMaintenanceTest) +#include "test_database_maintenance.moc"