306 lines
15 KiB
C++
306 lines
15 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 <QSettings>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <cstring>
|
|
extern "C" {
|
|
#include "utun_instance.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);
|
|
QSettings config(core.path(), QSettings::IniFormat);
|
|
QCOMPARE(config.value("chatserver/storage_unit_size").toString(), QString("17M"));
|
|
QCOMPARE(config.value("chatserver/storage_total_size").toString(), QString("3G"));
|
|
}
|
|
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
|
|
}
|
|
void pairLimits_data() {
|
|
QTest::addColumn<int>("target"); QTest::addColumn<QByteArray>("key"); QTest::addColumn<QByteArray>("value");
|
|
QTest::addColumn<bool>("accepted");
|
|
for (int target : {int(GUI_SETTING_UI_STATE), int(GUI_SETTING_CHAT)}) {
|
|
const int capacity = target == GUI_SETTING_UI_STATE ? sizeof(save_ui_state_arg::data) : sizeof(chat_setting_arg::data);
|
|
const QByteArray prefix = QByteArray::number(target) + '-';
|
|
QTest::newRow((prefix + "null-key").constData()) << target << QByteArray() << QByteArray("v") << false;
|
|
QTest::newRow((prefix + "empty-key").constData()) << target << QByteArray("") << QByteArray("v") << false;
|
|
QTest::newRow((prefix + "null-value").constData()) << target << QByteArray("k") << QByteArray() << false;
|
|
QTest::newRow((prefix + "empty-value").constData()) << target << QByteArray("k") << QByteArray("") << true;
|
|
QTest::newRow((prefix + "exact-fit").constData()) << target << QByteArray("k") << QByteArray(capacity - 3, 'v') << true;
|
|
QTest::newRow((prefix + "long-value").constData()) << target << QByteArray("k") << QByteArray(capacity - 2, 'v') << false;
|
|
QTest::newRow((prefix + "long-key").constData()) << target << QByteArray(capacity, 'k') << QByteArray("") << false;
|
|
}
|
|
}
|
|
void pairLimits() {
|
|
QFETCH(int, target); QFETCH(QByteArray, key); QFETCH(QByteArray, value); QFETCH(bool, accepted);
|
|
SettingsCore core; QVERIFY(core.start());
|
|
const auto baseline = u_get_allocated_count();
|
|
QCOMPARE(gui_bridge_post_setting(static_cast<gui_setting_target>(target), key.isNull() ? nullptr : key.constData(),
|
|
value.isNull() ? nullptr : value.constData()), accepted ? 0 : -1);
|
|
gui_bridge_set_inst(nullptr);
|
|
QCOMPARE(u_get_allocated_count(), baseline);
|
|
}
|
|
void nameLimits_data() {
|
|
QTest::addColumn<QByteArray>("name"); QTest::addColumn<bool>("accepted");
|
|
QTest::newRow("null") << QByteArray() << false;
|
|
QTest::newRow("empty") << QByteArray("") << true;
|
|
QTest::newRow("exact-fit") << QByteArray(sizeof(update_my_name_arg::name) - 1, 'n') << true;
|
|
QTest::newRow("too-long") << QByteArray(sizeof(update_my_name_arg::name), 'n') << false;
|
|
}
|
|
void nameLimits() {
|
|
QFETCH(QByteArray, name); QFETCH(bool, accepted);
|
|
SettingsCore core; QVERIFY(core.start());
|
|
const auto baseline = u_get_allocated_count();
|
|
QCOMPARE(gui_bridge_update_name(name.isNull() ? nullptr : name.constData()), accepted ? 0 : -1);
|
|
gui_bridge_set_inst(nullptr);
|
|
QCOMPARE(u_get_allocated_count(), baseline);
|
|
}
|
|
void copiesInputsBeforeReturning() {
|
|
SettingsCore core; QVERIFY(core.start());
|
|
#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES
|
|
watching = true;
|
|
#endif
|
|
QByteArray key("snapshot"), value("before"), name("After"), setting("storage_autoload"), enabled("1");
|
|
QCOMPARE(gui_bridge_post_setting(GUI_SETTING_UI_STATE, key.constData(), value.constData()), 0);
|
|
QCOMPARE(gui_bridge_post_setting(GUI_SETTING_CHAT, setting.constData(), enabled.constData()), 0);
|
|
QCOMPARE(gui_bridge_update_name(name.constData()), 0);
|
|
key.fill('x'); value.fill('x'); name.fill('x'); setting.fill('x'); enabled.fill('0');
|
|
uasync_poll(core.ua, 0);
|
|
QCOMPARE(core.db.getUiState("snapshot"), QString("before"));
|
|
QCOMPARE(chat_setting_get_int(core.inst, "storage_autoload", -1), 1);
|
|
QCOMPARE(QString::fromUtf8(core.inst->name), QString("After"));
|
|
#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}) {
|
|
const int count = kind == Storage ? 4 : kind == Audio ? 22 : payload ? 38 : 56;
|
|
QList<int> positions{1, 2, count};
|
|
if (count / 2 > 2) positions.append(count / 2);
|
|
for (int position : positions)
|
|
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));
|
|
}
|
|
void nameAllocationFailure_data() {
|
|
QTest::addColumn<bool>("payload");
|
|
QTest::newRow("payload") << true; QTest::newRow("queue") << false;
|
|
}
|
|
void nameAllocationFailure() {
|
|
QFETCH(bool, payload);
|
|
SettingsCore core; QVERIFY(core.start());
|
|
watching = true; failMalloc = payload; failAllocation = 1;
|
|
QCOMPARE(gui_bridge_update_name("After"), -1);
|
|
QCOMPARE(failAllocation, 0);
|
|
QCOMPARE(outstanding.size(), size_t(0));
|
|
QVERIFY(!core.ua->posted_tasks_head);
|
|
}
|
|
#endif
|
|
};
|
|
|
|
QTEST_MAIN(SettingsSaveTest)
|
|
#include "test_settings_save.moc"
|