229 lines
10 KiB
C++
229 lines
10 KiB
C++
/* Реальные страницы/диалог/bridge/SQLite: запросы переживают форму, освобождаются при отказе/отмене/OOM. */
|
|
#include "../src/storagesettingspage.h"
|
|
#include "../src/audiodevicesettingspage.h"
|
|
#include "../src/settingsdialog.h"
|
|
#include "../src/radio_audio_engine.h"
|
|
#include "../src/voicemessageencoder.h"
|
|
#include "../db/db_manager.h"
|
|
#include "../transport/gui_bridge.h"
|
|
#include <QtTest>
|
|
#include <QTemporaryDir>
|
|
#include <QFile>
|
|
#include <QMessageBox>
|
|
#include <QCheckBox>
|
|
#include <QSpinBox>
|
|
#include <QScopeGuard>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <cstring>
|
|
extern "C" {
|
|
#include "utun_instance.h"
|
|
#include "config_parser.h"
|
|
#include "chat/chat_core.h"
|
|
#include "chat/chat_setting.h"
|
|
#include "mem.h"
|
|
#include "debug_config.h"
|
|
}
|
|
|
|
#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES
|
|
static bool watching, failMalloc;
|
|
static int failAllocation;
|
|
static std::map<void*, const char*> outstanding;
|
|
static bool commandSource(const char* where) {
|
|
return strstr(where, "/gui_bridge_impl.cpp:") || strstr(where, "/settingsdialog.cpp:") ||
|
|
strstr(where, "/storagesettingspage.cpp:") || strstr(where, "/audiodevicesettingspage.cpp:");
|
|
}
|
|
static bool rejectAllocation(const char* where, bool mallocCall) {
|
|
if (!failAllocation || mallocCall != failMalloc || !strstr(where, "/gui_bridge_impl.cpp:") || --failAllocation) return false;
|
|
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "[INJECT] settings allocation failure malloc=%d at %s", mallocCall, 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) {
|
|
if (rejectAllocation(where, true)) return nullptr;
|
|
void* ptr = __real_u_malloc_impl(bytes, where);
|
|
if (ptr && watching && commandSource(where)) outstanding.emplace(ptr, where);
|
|
return ptr;
|
|
}
|
|
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) {
|
|
if (rejectAllocation(where, false)) return nullptr;
|
|
void* ptr = __real_u_calloc_impl(count, bytes, where);
|
|
if (ptr && watching && commandSource(where)) outstanding.emplace(ptr, where);
|
|
return ptr;
|
|
}
|
|
extern "C" void __real_u_free_impl(void*, const char*);
|
|
extern "C" void __wrap_u_free_impl(void* ptr, const char* where) {
|
|
if (watching) outstanding.erase(ptr);
|
|
__real_u_free_impl(ptr, where);
|
|
}
|
|
#endif
|
|
|
|
struct SettingsCore {
|
|
QTemporaryDir directory;
|
|
UASYNC* ua = uasync_create();
|
|
UTUN_INSTANCE* inst = nullptr;
|
|
DbManager db;
|
|
QString path() const { return directory.filePath("utun.cfg"); }
|
|
~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() {
|
|
if (!ua || !directory.isValid()) return false;
|
|
const QByteArray text = "[global]\n"
|
|
"my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n"
|
|
"my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n"
|
|
"my_node_name=Before\n[chatserver]\ngroup_autoconnect=0\nmember_files_enabled=0\nstorage_autoload=0\n";
|
|
QFile file(path());
|
|
if (!file.open(QIODevice::WriteOnly) || file.write(text) != text.size()) return false;
|
|
file.close();
|
|
inst = utun_instance_create_from_str(ua, text.constData());
|
|
if (!inst || utun_core_start(inst) || chat_service_start(inst)) return false;
|
|
db.setDb(inst->topo_sqlite_db); db.setMyNodeId(inst->node_id);
|
|
gui_bridge_set_uasync(ua); gui_bridge_set_inst(inst);
|
|
RadioAudioEngine::instance()->loadSettings(path());
|
|
uasync_poll(ua, 0); QCoreApplication::processEvents();
|
|
return RadioAudioEngine::instance()->settingsReady();
|
|
}
|
|
};
|
|
|
|
class SettingsSaveTest : public QObject {
|
|
Q_OBJECT
|
|
enum Page { Storage, Audio, Dialog };
|
|
std::unique_ptr<QWidget> createPage(SettingsCore& core, int kind) {
|
|
std::unique_ptr<QWidget> result;
|
|
if (kind == Storage) {
|
|
auto* page = new StorageSettingsPage(&core.db);
|
|
page->setConfigPath(core.path()); page->loadFromDb(); result.reset(page);
|
|
} else if (kind == Audio) {
|
|
auto* page = new AudioDeviceSettingsPage(&core.db, nullptr);
|
|
page->setConfigPath(core.path()); page->loadFromDb(); result.reset(page);
|
|
} else {
|
|
auto* page = new SettingsDialog(core.path(), &core.db, nullptr, nullptr);
|
|
for (auto* edit : page->findChildren<QLineEdit*>()) if (edit->text() == "Before") edit->setText("After");
|
|
page->findChild<QSpinBox*>("streamSubblocks")->setValue(12);
|
|
page->findChild<QSpinBox*>("streamRedundant")->setValue(3);
|
|
result.reset(page);
|
|
}
|
|
auto* storage = kind == Storage ? static_cast<StorageSettingsPage*>(result.get()) : result->findChild<StorageSettingsPage*>();
|
|
if (storage) {
|
|
storage->findChild<QCheckBox*>()->setChecked(true);
|
|
const auto spins = storage->findChildren<QSpinBox*>();
|
|
spins[0]->setValue(17); spins[1]->setValue(3);
|
|
}
|
|
auto* audio = kind == Audio ? static_cast<AudioDeviceSettingsPage*>(result.get()) : result->findChild<AudioDeviceSettingsPage*>();
|
|
if (audio) {
|
|
const auto combos = audio->findChildren<QComboBox*>();
|
|
combos[2]->setCurrentIndex(combos[2]->findData(2));
|
|
}
|
|
return result;
|
|
}
|
|
void apply(QWidget* page, int kind) {
|
|
if (kind == Storage) static_cast<StorageSettingsPage*>(page)->applyAndSave();
|
|
else if (kind == Audio) static_cast<AudioDeviceSettingsPage*>(page)->applyAndSave();
|
|
else QVERIFY(QMetaObject::invokeMethod(page, "onSave", Qt::DirectConnection));
|
|
}
|
|
void checkSaved(SettingsCore& core, int kind) {
|
|
if (kind != Audio) {
|
|
QCOMPARE(core.db.getUiStateInt("storage_autoload", -1), 1);
|
|
QCOMPARE(core.db.getUiStateInt("storage_autoload_maxsize_mb", -1), 17);
|
|
QCOMPARE(core.db.getUiStateInt("storage_maxsize_gb", -1), 3);
|
|
QCOMPARE(chat_setting_get_int(core.inst, "storage_autoload", -1), 1);
|
|
}
|
|
if (kind != Storage) {
|
|
QCOMPARE(core.db.getUiStateInt("opus_codec_preset", -1), 2);
|
|
QCOMPARE(chat_setting_get_int(core.inst, "opus_codec_preset", -1), 2);
|
|
QVERIFY(!core.db.getUiState("call_video_bitrate_range_kbps").isEmpty());
|
|
QCOMPARE(core.db.getUiState("call_video_bitrate_range_kbps"), QString::fromUtf8(chat_setting_get(core.inst,
|
|
"call_video_bitrate_range_kbps")));
|
|
QCOMPARE(VoiceEncoder::preset(), 2);
|
|
}
|
|
if (kind == Dialog) {
|
|
QCOMPARE(QString::fromUtf8(core.inst->name), QString("After"));
|
|
QCOMPARE(chat_setting_get_int(core.inst, "stream_subblocks", -1), 12);
|
|
QCOMPARE(chat_setting_get_int(core.inst, "stream_rs_redundant", -1), 3);
|
|
QCOMPARE(core.db.getUiStateInt("join_policy", -1), 1);
|
|
QCOMPARE(core.db.getUiStateInt("group_autoconnect", -1), 1);
|
|
}
|
|
}
|
|
private slots:
|
|
void initTestCase() {
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); gui_bridge_init();
|
|
debug_set_category_level(DEBUG_CATEGORY_CONFIG, DEBUG_LEVEL_DEBUG);
|
|
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG);
|
|
auto* warnings = new QTimer(this);
|
|
connect(warnings, &QTimer::timeout, [] {
|
|
if (auto* box = qobject_cast<QMessageBox*>(QApplication::activeModalWidget())) box->accept();
|
|
});
|
|
warnings->start(10);
|
|
}
|
|
void init() {
|
|
#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES
|
|
watching = false; outstanding.clear(); failAllocation = 0;
|
|
#endif
|
|
}
|
|
void cleanup() {
|
|
#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES
|
|
watching = false; failAllocation = 0; outstanding.clear();
|
|
#endif
|
|
}
|
|
void lifecycle_data() {
|
|
QTest::addColumn<int>("kind"); QTest::addColumn<int>("outcome");
|
|
for (int kind : {Storage, Audio, Dialog})
|
|
for (int outcome = 0; outcome < 3; ++outcome)
|
|
QTest::newRow(qPrintable(QString("%1-%2").arg(kind).arg(outcome))) << kind << outcome;
|
|
}
|
|
void lifecycle() {
|
|
QFETCH(int, kind); QFETCH(int, outcome);
|
|
SettingsCore core; QVERIFY(core.start());
|
|
auto page = createPage(core, kind);
|
|
if (!outcome) gui_bridge_set_inst(nullptr); // Отказ при недоступном ядре.
|
|
const auto baseline = u_get_allocated_count();
|
|
#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES
|
|
watching = true;
|
|
#endif
|
|
apply(page.get(), kind);
|
|
page.reset(); // Снимок запросов не должен зависеть от формы.
|
|
if (outcome == 2) { uasync_poll(core.ua, 0); checkSaved(core, kind); }
|
|
else {
|
|
gui_bridge_set_inst(nullptr);
|
|
QCOMPARE(core.db.getUiStateInt("storage_autoload", -1), -1);
|
|
QCOMPARE(core.db.getUiStateInt("opus_codec_preset", -1), -1);
|
|
QCOMPARE(u_get_allocated_count(), baseline);
|
|
}
|
|
#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES
|
|
QCOMPARE(outstanding.size(), size_t(0));
|
|
#endif
|
|
}
|
|
#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES
|
|
void allocationFailure_data() {
|
|
QTest::addColumn<int>("kind"); QTest::addColumn<bool>("payload"); QTest::addColumn<int>("position");
|
|
QTest::addColumn<bool>("cancel");
|
|
for (int kind : {Storage, Audio, Dialog})
|
|
for (bool payload : {false, true})
|
|
for (int position : {1, 2})
|
|
for (bool cancel : {false, true})
|
|
QTest::newRow(qPrintable(QString("%1-%2-%3-%4").arg(kind).arg(payload).arg(position).arg(cancel)))
|
|
<< kind << payload << position << cancel;
|
|
}
|
|
void allocationFailure() {
|
|
QFETCH(int, kind); QFETCH(bool, payload); QFETCH(int, position); QFETCH(bool, cancel);
|
|
SettingsCore core; QVERIFY(core.start());
|
|
auto page = createPage(core, kind);
|
|
watching = true; failMalloc = payload; failAllocation = position;
|
|
apply(page.get(), kind);
|
|
QCOMPARE(failAllocation, 0);
|
|
if (cancel) gui_bridge_set_inst(nullptr);
|
|
else uasync_poll(core.ua, 0);
|
|
QCOMPARE(outstanding.size(), size_t(0));
|
|
}
|
|
#endif
|
|
};
|
|
|
|
QTEST_MAIN(SettingsSaveTest)
|
|
#include "test_settings_save.moc"
|