refactor(config,audio): merge config into oakcommon, split oakaudio
- config moves into oakcommon as ConfigStore + oakcommon_config_* C API (INI storage, typed entries, error-handler injection); node and render call sites keep OAK_CONFIG() macro shape via a local shim that forwards to the C API; transition config stubs removed - oakaudio: de-Qt all six classes, C ABI in include/audio with refcounted handles (processor/manager/waveform/levelmeter/sync, 48 functions); PreviewAudioDevice moved in from render; recording goes through oakcodec encoder; waveform extract uses probe + ffmpeg_bridge decode (decode_audio needs M8 task system) - fix re_sum_samples min/max init bug (values clamped to 0 for same-sign ranges) - every C API function has positive + error-path tests; suites: oakcommon 193, oakaudio 36, oaknode 96, oakrender 42, oakcodec 18
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
target_sources(oakcommon PRIVATE
|
||||
colortransform.cpp
|
||||
config.cpp
|
||||
commandlineparser.cpp
|
||||
current.cpp
|
||||
subtitleparams.cpp
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "common/config.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#include "../src/configstore.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::mutex handler_mutex;
|
||||
OakCommonConfigErrorHandler error_handler = nullptr;
|
||||
void *error_handler_userdata = nullptr;
|
||||
|
||||
bool is_valid_key(const char *key)
|
||||
{
|
||||
return key != nullptr && key[0] != '\0';
|
||||
}
|
||||
|
||||
bool is_valid_string_out(char *buf, int buf_size)
|
||||
{
|
||||
return buf_size >= 0 && (buf_size == 0 || buf != nullptr);
|
||||
}
|
||||
|
||||
int write_string_result(const std::string &value, char *buf, int buf_size)
|
||||
{
|
||||
int required = static_cast<int>(value.size()) + 1;
|
||||
if (buf != nullptr && buf_size >= required) {
|
||||
memcpy(buf, value.c_str(), required);
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
int to_c_type(ConfigStore::Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case ConfigStore::Type::k_string:
|
||||
return OAKCOMMON_CONFIG_ENTRY_STRING;
|
||||
case ConfigStore::Type::k_int:
|
||||
return OAKCOMMON_CONFIG_ENTRY_INT;
|
||||
case ConfigStore::Type::k_double:
|
||||
return OAKCOMMON_CONFIG_ENTRY_DOUBLE;
|
||||
case ConfigStore::Type::k_bool:
|
||||
return OAKCOMMON_CONFIG_ENTRY_BOOL;
|
||||
default:
|
||||
return OAKCOMMON_CONFIG_ENTRY_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int oakcommon_config_load(void)
|
||||
{
|
||||
try {
|
||||
return ConfigStore::current().load() ? OAKCOMMON_OK
|
||||
: OAKCOMMON_E_FAILED;
|
||||
} catch (...) {
|
||||
return OAKCOMMON_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakcommon_config_save(void)
|
||||
{
|
||||
try {
|
||||
return ConfigStore::current().save() ? OAKCOMMON_OK
|
||||
: OAKCOMMON_E_FAILED;
|
||||
} catch (...) {
|
||||
return OAKCOMMON_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakcommon_config_reset_defaults(void)
|
||||
{
|
||||
try {
|
||||
ConfigStore::current().set_defaults();
|
||||
return OAKCOMMON_OK;
|
||||
} catch (...) {
|
||||
return OAKCOMMON_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
void oakcommon_config_set(const char *group, const char *key,
|
||||
const char *value_utf8)
|
||||
{
|
||||
if (!is_valid_key(key) || value_utf8 == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ConfigStore &store = ConfigStore::current();
|
||||
const std::string joined = ConfigStore::join_key(group, key);
|
||||
const ConfigStore::Entry *existing = store.get(joined);
|
||||
if (existing == nullptr ||
|
||||
existing->type == ConfigStore::Type::k_string) {
|
||||
ConfigStore::Entry e;
|
||||
e.type = ConfigStore::Type::k_string;
|
||||
e.string_value = value_utf8;
|
||||
store.set(joined, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Existing typed entry: parse the string into its declared type;
|
||||
// an unparseable value leaves the entry unchanged.
|
||||
ConfigStore::Entry parsed;
|
||||
if (ConfigStore::string_to_value(value_utf8, existing->type,
|
||||
&parsed)) {
|
||||
store.set(joined, parsed);
|
||||
}
|
||||
} catch (...) {
|
||||
// §2.1 setters return void; allocation failures are swallowed.
|
||||
}
|
||||
}
|
||||
|
||||
int oakcommon_config_get(const char *group, const char *key, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!is_valid_key(key) || !is_valid_string_out(buf, buf_size)) {
|
||||
return OAKCOMMON_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
const ConfigStore::Entry *entry =
|
||||
ConfigStore::current().get(ConfigStore::join_key(group, key));
|
||||
if (entry == nullptr) {
|
||||
return OAKCOMMON_E_NOT_FOUND;
|
||||
}
|
||||
return write_string_result(ConfigStore::value_to_string(*entry), buf,
|
||||
buf_size);
|
||||
} catch (...) {
|
||||
return OAKCOMMON_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakcommon_config_get_int(const char *group, const char *key,
|
||||
int fallback)
|
||||
{
|
||||
return static_cast<int>(
|
||||
oakcommon_config_get_int64(group, key, fallback));
|
||||
}
|
||||
|
||||
int64_t oakcommon_config_get_int64(const char *group, const char *key,
|
||||
int64_t fallback)
|
||||
{
|
||||
if (!is_valid_key(key)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
const ConfigStore::Entry *entry =
|
||||
ConfigStore::current().get(ConfigStore::join_key(group, key));
|
||||
if (entry == nullptr || entry->type != ConfigStore::Type::k_int) {
|
||||
return fallback;
|
||||
}
|
||||
return entry->int_value;
|
||||
} catch (...) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
void oakcommon_config_set_int(const char *group, const char *key, int v)
|
||||
{
|
||||
oakcommon_config_set_int64(group, key, v);
|
||||
}
|
||||
|
||||
void oakcommon_config_set_int64(const char *group, const char *key,
|
||||
int64_t v)
|
||||
{
|
||||
if (!is_valid_key(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ConfigStore::Entry e;
|
||||
e.type = ConfigStore::Type::k_int;
|
||||
e.int_value = v;
|
||||
ConfigStore::current().set(ConfigStore::join_key(group, key), e);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
double oakcommon_config_get_double(const char *group, const char *key,
|
||||
double fallback)
|
||||
{
|
||||
if (!is_valid_key(key)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
const ConfigStore::Entry *entry =
|
||||
ConfigStore::current().get(ConfigStore::join_key(group, key));
|
||||
if (entry == nullptr || entry->type != ConfigStore::Type::k_double) {
|
||||
return fallback;
|
||||
}
|
||||
return entry->double_value;
|
||||
} catch (...) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
void oakcommon_config_set_double(const char *group, const char *key,
|
||||
double v)
|
||||
{
|
||||
if (!is_valid_key(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ConfigStore::Entry e;
|
||||
e.type = ConfigStore::Type::k_double;
|
||||
e.double_value = v;
|
||||
ConfigStore::current().set(ConfigStore::join_key(group, key), e);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
int oakcommon_config_get_bool(const char *group, const char *key,
|
||||
int fallback)
|
||||
{
|
||||
if (!is_valid_key(key)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
const ConfigStore::Entry *entry =
|
||||
ConfigStore::current().get(ConfigStore::join_key(group, key));
|
||||
if (entry == nullptr || entry->type != ConfigStore::Type::k_bool) {
|
||||
return fallback;
|
||||
}
|
||||
return entry->bool_value ? 1 : 0;
|
||||
} catch (...) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
void oakcommon_config_set_bool(const char *group, const char *key, int v)
|
||||
{
|
||||
if (!is_valid_key(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ConfigStore::Entry e;
|
||||
e.type = ConfigStore::Type::k_bool;
|
||||
e.bool_value = v != 0;
|
||||
ConfigStore::current().set(ConfigStore::join_key(group, key), e);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
int oakcommon_config_entry_type(const char *group, const char *key)
|
||||
{
|
||||
if (!is_valid_key(key)) {
|
||||
return OAKCOMMON_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
const ConfigStore::Entry *entry =
|
||||
ConfigStore::current().get(ConfigStore::join_key(group, key));
|
||||
if (entry == nullptr) {
|
||||
return OAKCOMMON_E_NOT_FOUND;
|
||||
}
|
||||
return to_c_type(entry->type);
|
||||
} catch (...) {
|
||||
return OAKCOMMON_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakcommon_config_set_error_handler(OakCommonConfigErrorHandler handler,
|
||||
void *userdata)
|
||||
{
|
||||
try {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(handler_mutex);
|
||||
error_handler = handler;
|
||||
error_handler_userdata = userdata;
|
||||
}
|
||||
|
||||
if (handler != nullptr) {
|
||||
ConfigStore::set_error_handler(
|
||||
[](const std::string &title, const std::string &message) {
|
||||
std::lock_guard<std::mutex> lock(handler_mutex);
|
||||
if (error_handler != nullptr) {
|
||||
error_handler(title.c_str(), message.c_str(),
|
||||
error_handler_userdata);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ConfigStore::set_error_handler(nullptr);
|
||||
}
|
||||
return OAKCOMMON_OK;
|
||||
} catch (...) {
|
||||
return OAKCOMMON_E_FAILED;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ add_library(oakcommon SHARED
|
||||
commandlineparser.cpp
|
||||
commandlineparser.h
|
||||
colortransform.h
|
||||
configstore.cpp
|
||||
configstore.h
|
||||
current.cpp
|
||||
current.h
|
||||
debug.cpp
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "configstore.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <system_error>
|
||||
|
||||
#include "filefunctions.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
ConfigStore::ErrorHandler ConfigStore::error_handler_ = nullptr;
|
||||
|
||||
ConfigStore &ConfigStore::current()
|
||||
{
|
||||
static ConfigStore store;
|
||||
return store;
|
||||
}
|
||||
|
||||
ConfigStore::ConfigStore()
|
||||
{
|
||||
set_defaults();
|
||||
}
|
||||
|
||||
void ConfigStore::set_error_handler(ErrorHandler handler)
|
||||
{
|
||||
error_handler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void ConfigStore::report_error(const std::string &title,
|
||||
const std::string &message)
|
||||
{
|
||||
if (error_handler_) {
|
||||
error_handler_(title, message);
|
||||
} else {
|
||||
fprintf(stderr, "%s: %s\n", title.c_str(), message.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
std::string ConfigStore::get_config_file_path()
|
||||
{
|
||||
return (fs::path(FileFunctions::get_configuration_location()) /
|
||||
"config.ini")
|
||||
.string();
|
||||
}
|
||||
|
||||
std::string ConfigStore::join_key(const char *group, const char *key)
|
||||
{
|
||||
if (group != nullptr && group[0] != '\0') {
|
||||
return std::string(group) + "/" + key;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
void ConfigStore::set_defaults()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
config_map_.clear();
|
||||
|
||||
auto set_string = [this](const char *key, const char *value) {
|
||||
Entry e;
|
||||
e.type = Type::k_string;
|
||||
e.string_value = value;
|
||||
config_map_[key] = e;
|
||||
};
|
||||
auto set_int = [this](const char *key, int64_t value) {
|
||||
Entry e;
|
||||
e.type = Type::k_int;
|
||||
e.int_value = value;
|
||||
config_map_[key] = e;
|
||||
};
|
||||
auto set_bool = [this](const char *key, bool value) {
|
||||
Entry e;
|
||||
e.type = Type::k_bool;
|
||||
e.bool_value = value;
|
||||
config_map_[key] = e;
|
||||
};
|
||||
|
||||
// Only the keys the de-Qt engine modules (oaknode/oakrender/oakcodec)
|
||||
// actually read are registered here; the app-layer keys of the old Qt
|
||||
// config arrive with the app/config wave. Enum-valued ints hardcode
|
||||
// the numeric values of their (still Qt-based) defining headers:
|
||||
//
|
||||
// - Timeline::k_thumbnail_in_out / k_waveforms_enabled = 1
|
||||
// (engine/timeline/timelinecommon.h)
|
||||
// - PixelFormat::f32 = 4 (core/include/olive/core/render/pixelformat.h)
|
||||
// - VideoParams::k_interlace_none = 0 (src/common/src/videoparams.h)
|
||||
// - k_channel_layout_stereo = 3
|
||||
// (core/include/olive/core/render/channellayout.h)
|
||||
// - ColorCoding::k_red..k_navy = 0..11, k_lime = 6
|
||||
// (engine/ui/colorcoding.h)
|
||||
|
||||
set_int("TimelineThumbnailMode", 1);
|
||||
set_int("TimelineWaveformMode", 1);
|
||||
|
||||
set_int("DefaultSequenceWidth", 1920);
|
||||
set_int("DefaultSequenceHeight", 1080);
|
||||
// Rational settings are stored as strings in oakcore_rational
|
||||
// "num/den" form; this mirrors the old default Rational(1001, 30000).
|
||||
set_string("DefaultSequenceFrameRate", "1001/30000");
|
||||
set_string("DefaultSequencePixelAspect", "1/1");
|
||||
set_int("DefaultSequenceInterlacing", 0);
|
||||
set_int("DefaultSequenceAudioFrequency", 48000);
|
||||
set_int("DefaultSequenceAudioLayout", 3);
|
||||
set_int("OfflinePixelFormat", 4);
|
||||
|
||||
set_bool("SplitClipsCopyNodes", true);
|
||||
set_bool("UseProxyMedia", true);
|
||||
set_bool("UseGLFinish", false);
|
||||
set_bool("ReassocLinToNonLin", false);
|
||||
|
||||
set_string("GraphicsBackend", "opengl");
|
||||
set_string("LUTLibraryPaths", "");
|
||||
|
||||
set_int("DiskCacheSaveInterval", 10000);
|
||||
set_int("AutoCacheDelay", 1000);
|
||||
set_string("DiskCacheBehind", "0/1");
|
||||
set_string("DiskCacheAhead", "60/1");
|
||||
|
||||
set_int("ProxyWidth", 1280);
|
||||
set_int("ProxyHeight", 720);
|
||||
set_int("ProxyDivider", 1);
|
||||
set_int("ProxyCRF", 23);
|
||||
set_string("ProxyPreset", "veryfast");
|
||||
set_bool("ProxyIncludeAudio", true);
|
||||
|
||||
set_int("MarkerColor", 6);
|
||||
for (int i = 0; i <= 11; i++) {
|
||||
set_int(("CatColor" + std::to_string(i)).c_str(), i);
|
||||
}
|
||||
}
|
||||
|
||||
std::string ConfigStore::value_to_string(const Entry &entry)
|
||||
{
|
||||
switch (entry.type) {
|
||||
case Type::k_string:
|
||||
return entry.string_value;
|
||||
case Type::k_int:
|
||||
return std::to_string(entry.int_value);
|
||||
case Type::k_double: {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%g", entry.double_value);
|
||||
return buf;
|
||||
}
|
||||
case Type::k_bool:
|
||||
return entry.bool_value ? "true" : "false";
|
||||
default:
|
||||
return std::string();
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigStore::string_to_value(const std::string &text, Type type,
|
||||
Entry *out)
|
||||
{
|
||||
Entry e;
|
||||
e.type = type;
|
||||
|
||||
switch (type) {
|
||||
case Type::k_string:
|
||||
e.string_value = text;
|
||||
break;
|
||||
case Type::k_int: {
|
||||
try {
|
||||
size_t pos = 0;
|
||||
e.int_value = std::stoll(text, &pos);
|
||||
if (pos != text.size()) {
|
||||
return false;
|
||||
}
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Type::k_double: {
|
||||
try {
|
||||
size_t pos = 0;
|
||||
e.double_value = std::stod(text, &pos);
|
||||
if (pos != text.size()) {
|
||||
return false;
|
||||
}
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Type::k_bool:
|
||||
if (text == "true" || text == "1") {
|
||||
e.bool_value = true;
|
||||
} else if (text == "false" || text == "0") {
|
||||
e.bool_value = false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
*out = e;
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string trim(const std::string &s)
|
||||
{
|
||||
const size_t first = s.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string::npos) {
|
||||
return std::string();
|
||||
}
|
||||
const size_t last = s.find_last_not_of(" \t\r\n");
|
||||
return s.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ConfigStore::load()
|
||||
{
|
||||
set_defaults();
|
||||
|
||||
const std::string path = get_config_file_path();
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(path, ec)) {
|
||||
// No saved settings yet: defaults are fine, not an error.
|
||||
return true;
|
||||
}
|
||||
|
||||
// exists() also covers directories, which ifstream would happily
|
||||
// "open" on POSIX; only a regular file is a readable config.
|
||||
if (!fs::is_regular_file(path, ec)) {
|
||||
report_error("Error loading settings",
|
||||
"Failed to load application settings. This session will "
|
||||
"use defaults.");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream in(path);
|
||||
if (!in.is_open()) {
|
||||
report_error("Error loading settings",
|
||||
"Failed to load application settings. This session will "
|
||||
"use defaults.");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string group;
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
line = trim(line);
|
||||
if (line.empty() || line.front() == ';' || line.front() == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.front() == '[' && line.back() == ']') {
|
||||
group = trim(line.substr(1, line.size() - 2));
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t eq = line.find('=');
|
||||
if (eq == std::string::npos) {
|
||||
// Malformed line: skip, keep going (matches QSettings' lax
|
||||
// INI parsing).
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string key = trim(line.substr(0, eq));
|
||||
const std::string value = trim(line.substr(eq + 1));
|
||||
if (key.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (!group.empty()) {
|
||||
key = group + "/" + key;
|
||||
}
|
||||
|
||||
Entry parsed;
|
||||
const Entry *existing = get(key);
|
||||
if (existing != nullptr) {
|
||||
// Known key: honor its declared type. An unparseable value
|
||||
// keeps the default.
|
||||
if (string_to_value(value, existing->type, &parsed)) {
|
||||
set(key, parsed);
|
||||
}
|
||||
} else {
|
||||
// Unknown key: stored as a string.
|
||||
parsed.type = Type::k_string;
|
||||
parsed.string_value = value;
|
||||
set(key, parsed);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigStore::save()
|
||||
{
|
||||
const std::string real_filename = get_config_file_path();
|
||||
const std::string temp_filename = real_filename + ".tmp";
|
||||
|
||||
// Flat keys are written at the top level; keys containing '/' become
|
||||
// [group] sections (group = everything before the last '/'), keeping
|
||||
// the QSettings INI key shape.
|
||||
std::map<std::string, std::map<std::string, std::string>> sections;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (const auto &pair : config_map_) {
|
||||
const std::string &key = pair.first;
|
||||
const size_t slash = key.rfind('/');
|
||||
std::string group = slash == std::string::npos
|
||||
? std::string()
|
||||
: key.substr(0, slash);
|
||||
std::string sub = slash == std::string::npos
|
||||
? key
|
||||
: key.substr(slash + 1);
|
||||
sections[group][sub] = value_to_string(pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::ofstream out(temp_filename, std::ios::trunc);
|
||||
if (!out.is_open()) {
|
||||
report_error("Error saving settings",
|
||||
"Failed to save application settings. The "
|
||||
"application may lack write permissions for this "
|
||||
"location.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto flat = sections.find(std::string());
|
||||
if (flat != sections.end()) {
|
||||
for (const auto &pair : flat->second) {
|
||||
out << pair.first << '=' << pair.second << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto §ion : sections) {
|
||||
if (section.first.empty()) {
|
||||
continue;
|
||||
}
|
||||
out << '\n'
|
||||
<< '[' << section.first << ']' << '\n';
|
||||
for (const auto &pair : section.second) {
|
||||
out << pair.first << '=' << pair.second << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
out.flush();
|
||||
if (!out.good()) {
|
||||
report_error("Error saving settings",
|
||||
"Failed to save application settings. The "
|
||||
"application may lack write permissions for this "
|
||||
"location.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
fs::rename(temp_filename, real_filename, ec);
|
||||
if (ec) {
|
||||
fs::remove(real_filename, ec);
|
||||
ec.clear();
|
||||
fs::rename(temp_filename, real_filename, ec);
|
||||
if (ec) {
|
||||
report_error("Error saving settings",
|
||||
"Failed to overwrite the application settings "
|
||||
"file.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const ConfigStore::Entry *ConfigStore::get(const std::string &key) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = config_map_.find(key);
|
||||
return it == config_map_.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
void ConfigStore::set(const std::string &key, const Entry &entry)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
config_map_[key] = entry;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_CONFIGSTORE_H
|
||||
#define OAK_CONFIGSTORE_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* @brief De-Qt application configuration store (process singleton)
|
||||
*
|
||||
* Replacement for the Qt-based olive::Config (engine/config/config.h):
|
||||
* QMap/QVariant became std::map + a small typed value, QSettings became a
|
||||
* self-written INI file, and NodeValue::Type became ConfigStore::Type so
|
||||
* config no longer depends on the node module.
|
||||
*
|
||||
* Keys keep the QSettings INI shape: "group/key" maps to an INI [group]
|
||||
* section; flat keys stay at the top level. The file lives at
|
||||
* <FileFunctions::get_configuration_location()>/config.ini (the
|
||||
* OAK_CONFIG_DIR override applies, which is what tests use).
|
||||
*
|
||||
* All public methods are thread-safe (single mutex).
|
||||
*/
|
||||
class ConfigStore {
|
||||
public:
|
||||
enum class Type { k_none, k_string, k_int, k_double, k_bool };
|
||||
|
||||
struct Entry {
|
||||
Type type = Type::k_none;
|
||||
std::string string_value;
|
||||
int64_t int_value = 0;
|
||||
double double_value = 0.0;
|
||||
bool bool_value = false;
|
||||
};
|
||||
|
||||
using ErrorHandler =
|
||||
std::function<void(const std::string &title, const std::string &message)>;
|
||||
|
||||
static ConfigStore ¤t();
|
||||
|
||||
/**
|
||||
* @brief Resets the store to compiled-in defaults (drops custom keys)
|
||||
*/
|
||||
void set_defaults();
|
||||
|
||||
/**
|
||||
* @brief Resets to defaults, then applies config.ini if it exists
|
||||
*
|
||||
* @return false when the file exists but could not be read (the error
|
||||
* is also reported through the registered error handler).
|
||||
*/
|
||||
bool load();
|
||||
|
||||
/**
|
||||
* @brief Writes the store to config.ini via temp file + rename
|
||||
*
|
||||
* @return false on failure (also reported through the error handler).
|
||||
*/
|
||||
bool save();
|
||||
|
||||
/**
|
||||
* @brief Returns the entry for key, or nullptr when absent
|
||||
*
|
||||
* Keys are the joined "group/key" form (or the bare key when group is
|
||||
* null/empty).
|
||||
*/
|
||||
const Entry *get(const std::string &key) const;
|
||||
|
||||
/**
|
||||
* @brief Creates or replaces an entry
|
||||
*/
|
||||
void set(const std::string &key, const Entry &entry);
|
||||
|
||||
static void set_error_handler(ErrorHandler handler);
|
||||
static void report_error(const std::string &title,
|
||||
const std::string &message);
|
||||
|
||||
/**
|
||||
* @brief <get_configuration_location()>/config.ini
|
||||
*/
|
||||
static std::string get_config_file_path();
|
||||
|
||||
/**
|
||||
* @brief Joins group and key into the stored "group/key" form
|
||||
*/
|
||||
static std::string join_key(const char *group, const char *key);
|
||||
|
||||
/**
|
||||
* @brief Serializes an entry for the INI file / string getter
|
||||
*/
|
||||
static std::string value_to_string(const Entry &entry);
|
||||
|
||||
/**
|
||||
* @brief Parses text into an entry of the given type
|
||||
*
|
||||
* @return false when the text cannot be parsed as the requested type
|
||||
* (strings always parse).
|
||||
*/
|
||||
static bool string_to_value(const std::string &text, Type type,
|
||||
Entry *out);
|
||||
|
||||
private:
|
||||
ConfigStore();
|
||||
|
||||
std::map<std::string, Entry> config_map_;
|
||||
mutable std::mutex mutex_;
|
||||
|
||||
static ErrorHandler error_handler_;
|
||||
};
|
||||
|
||||
#endif // OAK_CONFIGSTORE_H
|
||||
@@ -21,6 +21,7 @@ include(GoogleTest)
|
||||
add_executable(oakcommon-gtest
|
||||
colortransform_test.cpp
|
||||
commandlineparser_test.cpp
|
||||
config_test.cpp
|
||||
current_test.cpp
|
||||
debug_test.cpp
|
||||
dropworkflowbehavior_test.cpp
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "common/config.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
/**
|
||||
* @brief Redirects the config file into a fresh per-test temp directory
|
||||
*
|
||||
* ConfigStore resolves its file through
|
||||
* FileFunctions::get_configuration_location() on every load/save, and
|
||||
* that honors OAK_CONFIG_DIR, so setting the env var per test isolates
|
||||
* the on-disk state. The store itself is reset to defaults in SetUp.
|
||||
*/
|
||||
class ConfigTest : public testing::Test {
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
dir_ = fs::temp_directory_path() /
|
||||
fs::path("oakconfig_test_" + std::to_string(
|
||||
::testing::UnitTest::GetInstance()
|
||||
->random_seed()) +
|
||||
"_" + std::to_string(counter_++));
|
||||
fs::create_directories(dir_);
|
||||
setenv("OAK_CONFIG_DIR", dir_.string().c_str(), 1);
|
||||
ASSERT_EQ(oakcommon_config_reset_defaults(), OAKCOMMON_OK);
|
||||
ASSERT_EQ(oakcommon_config_set_error_handler(nullptr, nullptr),
|
||||
OAKCOMMON_OK);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
unsetenv("OAK_CONFIG_DIR");
|
||||
oakcommon_config_reset_defaults();
|
||||
oakcommon_config_set_error_handler(nullptr, nullptr);
|
||||
std::error_code ec;
|
||||
fs::remove_all(dir_, ec);
|
||||
}
|
||||
|
||||
std::string ini_contents()
|
||||
{
|
||||
std::ifstream in((dir_ / "config.ini").string());
|
||||
return std::string(std::istreambuf_iterator<char>(in),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
fs::path dir_;
|
||||
static int counter_;
|
||||
};
|
||||
|
||||
int ConfigTest::counter_ = 0;
|
||||
|
||||
// --- compiled-in defaults -------------------------------------------------
|
||||
|
||||
TEST_F(ConfigTest, DefaultsAreRegistered)
|
||||
{
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", -1),
|
||||
1920);
|
||||
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "SplitClipsCopyNodes", -1),
|
||||
1);
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "CatColor11", -1), 11);
|
||||
|
||||
char buf[64];
|
||||
ASSERT_GT(oakcommon_config_get(nullptr, "GraphicsBackend", buf,
|
||||
sizeof(buf)),
|
||||
0);
|
||||
EXPECT_STREQ(buf, "opengl");
|
||||
|
||||
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "DefaultSequenceWidth"),
|
||||
OAKCOMMON_CONFIG_ENTRY_INT);
|
||||
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "GraphicsBackend"),
|
||||
OAKCOMMON_CONFIG_ENTRY_STRING);
|
||||
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "UseProxyMedia"),
|
||||
OAKCOMMON_CONFIG_ENTRY_BOOL);
|
||||
}
|
||||
|
||||
// --- oakcommon_config_set / oakcommon_config_get (string) ------------------
|
||||
|
||||
TEST_F(ConfigTest, SetGetStringRoundtripTwoStage)
|
||||
{
|
||||
oakcommon_config_set(nullptr, "TestStringKey", "hello world");
|
||||
|
||||
// Stage 1: query the required size with a NULL buffer.
|
||||
const int required =
|
||||
oakcommon_config_get(nullptr, "TestStringKey", nullptr, 0);
|
||||
ASSERT_EQ(required, int(strlen("hello world")) + 1);
|
||||
|
||||
// Stage 2: fetch into a sufficiently large buffer.
|
||||
std::string buf(required, '\0');
|
||||
ASSERT_EQ(oakcommon_config_get(nullptr, "TestStringKey", buf.data(),
|
||||
required),
|
||||
required);
|
||||
EXPECT_STREQ(buf.c_str(), "hello world");
|
||||
|
||||
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "TestStringKey"),
|
||||
OAKCOMMON_CONFIG_ENTRY_STRING);
|
||||
}
|
||||
|
||||
TEST_F(ConfigTest, GetStringErrorPaths)
|
||||
{
|
||||
char buf[8];
|
||||
// Missing key.
|
||||
EXPECT_EQ(oakcommon_config_get(nullptr, "NoSuchKey", buf, sizeof(buf)),
|
||||
OAKCOMMON_E_NOT_FOUND);
|
||||
// NULL key.
|
||||
EXPECT_EQ(oakcommon_config_get(nullptr, nullptr, buf, sizeof(buf)),
|
||||
OAKCOMMON_E_INVALID);
|
||||
// Negative buffer size.
|
||||
EXPECT_EQ(oakcommon_config_get(nullptr, "GraphicsBackend", buf, -1),
|
||||
OAKCOMMON_E_INVALID);
|
||||
// set with NULL value is a no-op (void return, entry must not appear).
|
||||
oakcommon_config_set(nullptr, "IgnoredKey", nullptr);
|
||||
EXPECT_EQ(oakcommon_config_get(nullptr, "IgnoredKey", buf, sizeof(buf)),
|
||||
OAKCOMMON_E_NOT_FOUND);
|
||||
}
|
||||
|
||||
// --- group/key (§2.1 two-argument form) ------------------------------------
|
||||
|
||||
TEST_F(ConfigTest, GroupedKeyMapsToIniSection)
|
||||
{
|
||||
oakcommon_config_set("Audio", "Output", "coreaudio");
|
||||
|
||||
char buf[32];
|
||||
ASSERT_GT(oakcommon_config_get("Audio", "Output", buf, sizeof(buf)), 0);
|
||||
EXPECT_STREQ(buf, "coreaudio");
|
||||
|
||||
ASSERT_EQ(oakcommon_config_save(), OAKCOMMON_OK);
|
||||
const std::string ini = ini_contents();
|
||||
EXPECT_NE(ini.find("[Audio]"), std::string::npos);
|
||||
EXPECT_NE(ini.find("Output=coreaudio"), std::string::npos);
|
||||
|
||||
// Empty group behaves like NULL group (top-level key).
|
||||
oakcommon_config_set("", "FlatKey", "flat");
|
||||
ASSERT_GT(oakcommon_config_get(nullptr, "FlatKey", buf, sizeof(buf)), 0);
|
||||
EXPECT_STREQ(buf, "flat");
|
||||
}
|
||||
|
||||
// --- int family -------------------------------------------------------------
|
||||
|
||||
TEST_F(ConfigTest, IntRoundtrip)
|
||||
{
|
||||
oakcommon_config_set_int(nullptr, "TestIntKey", -42);
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "TestIntKey", 0), -42);
|
||||
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "TestIntKey"),
|
||||
OAKCOMMON_CONFIG_ENTRY_INT);
|
||||
// Overrides a compiled-in default.
|
||||
oakcommon_config_set_int(nullptr, "DefaultSequenceWidth", 3840);
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", 0),
|
||||
3840);
|
||||
}
|
||||
|
||||
TEST_F(ConfigTest, IntFallbackOnMissingOrWrongType)
|
||||
{
|
||||
// Missing key -> fallback.
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "NoSuchKey", 7), 7);
|
||||
// Wrong type (string entry) -> fallback.
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "GraphicsBackend", 9), 9);
|
||||
}
|
||||
|
||||
TEST_F(ConfigTest, Int64RoundtripAndFallback)
|
||||
{
|
||||
oakcommon_config_set_int64(nullptr, "TestInt64Key",
|
||||
INT64_C(5000000000));
|
||||
EXPECT_EQ(oakcommon_config_get_int64(nullptr, "TestInt64Key", 0),
|
||||
INT64_C(5000000000));
|
||||
// Missing key -> fallback; NULL key -> fallback.
|
||||
EXPECT_EQ(oakcommon_config_get_int64(nullptr, "NoSuchKey",
|
||||
INT64_C(-1)),
|
||||
INT64_C(-1));
|
||||
EXPECT_EQ(oakcommon_config_get_int64(nullptr, nullptr, INT64_C(-2)),
|
||||
INT64_C(-2));
|
||||
}
|
||||
|
||||
// --- double family ----------------------------------------------------------
|
||||
|
||||
TEST_F(ConfigTest, DoubleRoundtrip)
|
||||
{
|
||||
oakcommon_config_set_double(nullptr, "TestDoubleKey", 2.5);
|
||||
EXPECT_DOUBLE_EQ(oakcommon_config_get_double(nullptr, "TestDoubleKey", 0),
|
||||
2.5);
|
||||
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "TestDoubleKey"),
|
||||
OAKCOMMON_CONFIG_ENTRY_DOUBLE);
|
||||
}
|
||||
|
||||
TEST_F(ConfigTest, DoubleFallbackOnMissingOrWrongType)
|
||||
{
|
||||
EXPECT_DOUBLE_EQ(oakcommon_config_get_double(nullptr, "NoSuchKey", 1.5),
|
||||
1.5);
|
||||
// Wrong type (int entry) -> fallback.
|
||||
EXPECT_DOUBLE_EQ(
|
||||
oakcommon_config_get_double(nullptr, "DefaultSequenceWidth", 3.5),
|
||||
3.5);
|
||||
}
|
||||
|
||||
// --- bool family ------------------------------------------------------------
|
||||
|
||||
TEST_F(ConfigTest, BoolRoundtrip)
|
||||
{
|
||||
oakcommon_config_set_bool(nullptr, "TestBoolKey", 1);
|
||||
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "TestBoolKey", 0), 1);
|
||||
oakcommon_config_set_bool(nullptr, "TestBoolKey", 0);
|
||||
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "TestBoolKey", 1), 0);
|
||||
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "TestBoolKey"),
|
||||
OAKCOMMON_CONFIG_ENTRY_BOOL);
|
||||
}
|
||||
|
||||
TEST_F(ConfigTest, BoolFallbackOnMissingOrWrongType)
|
||||
{
|
||||
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "NoSuchKey", 1), 1);
|
||||
// Wrong type (string entry) -> fallback.
|
||||
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "GraphicsBackend", 1), 1);
|
||||
}
|
||||
|
||||
// --- typed set through oakcommon_config_set ---------------------------------
|
||||
|
||||
TEST_F(ConfigTest, SetStringParsesIntoDeclaredType)
|
||||
{
|
||||
// Existing INT entry: a parseable string updates the value...
|
||||
oakcommon_config_set(nullptr, "DefaultSequenceWidth", "2560");
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", 0),
|
||||
2560);
|
||||
// ...an unparseable one leaves the entry unchanged.
|
||||
oakcommon_config_set(nullptr, "DefaultSequenceWidth", "not-a-number");
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", 0),
|
||||
2560);
|
||||
}
|
||||
|
||||
// --- load / save / reset_defaults -------------------------------------------
|
||||
|
||||
TEST_F(ConfigTest, SaveLoadRoundtrip)
|
||||
{
|
||||
oakcommon_config_set_int(nullptr, "DefaultSequenceHeight", 2160);
|
||||
oakcommon_config_set("Session", "LastDir", "/tmp/media");
|
||||
oakcommon_config_set_bool(nullptr, "UseGLFinish", 1);
|
||||
ASSERT_EQ(oakcommon_config_save(), OAKCOMMON_OK);
|
||||
ASSERT_TRUE(fs::exists(dir_ / "config.ini"));
|
||||
|
||||
// Wipe in-memory state; defaults must come back...
|
||||
ASSERT_EQ(oakcommon_config_reset_defaults(), OAKCOMMON_OK);
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceHeight", 0),
|
||||
1080);
|
||||
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "UseGLFinish", -1), 0);
|
||||
char buf[64];
|
||||
EXPECT_EQ(oakcommon_config_get("Session", "LastDir", buf, sizeof(buf)),
|
||||
OAKCOMMON_E_NOT_FOUND);
|
||||
|
||||
// ...and load() restores everything that was saved.
|
||||
ASSERT_EQ(oakcommon_config_load(), OAKCOMMON_OK);
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceHeight", 0),
|
||||
2160);
|
||||
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "UseGLFinish", -1), 1);
|
||||
ASSERT_GT(oakcommon_config_get("Session", "LastDir", buf, sizeof(buf)),
|
||||
0);
|
||||
EXPECT_STREQ(buf, "/tmp/media");
|
||||
EXPECT_EQ(oakcommon_config_entry_type("Session", "LastDir"),
|
||||
OAKCOMMON_CONFIG_ENTRY_STRING);
|
||||
}
|
||||
|
||||
TEST_F(ConfigTest, LoadMissingFileIsNotAnError)
|
||||
{
|
||||
// Fresh directory, no config.ini: defaults stay, OAKCOMMON_OK.
|
||||
ASSERT_EQ(oakcommon_config_load(), OAKCOMMON_OK);
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", -1),
|
||||
1920);
|
||||
}
|
||||
|
||||
TEST_F(ConfigTest, LoadErrorPathUnreadableFile)
|
||||
{
|
||||
// Make config.ini a directory: it exists but cannot be read.
|
||||
fs::create_directories(dir_ / "config.ini");
|
||||
EXPECT_EQ(oakcommon_config_load(), OAKCOMMON_E_FAILED);
|
||||
}
|
||||
|
||||
TEST_F(ConfigTest, ResetDefaultsDropsCustomKeys)
|
||||
{
|
||||
oakcommon_config_set_int(nullptr, "TransientKey", 1);
|
||||
ASSERT_EQ(oakcommon_config_get_int(nullptr, "TransientKey", -1), 1);
|
||||
ASSERT_EQ(oakcommon_config_reset_defaults(), OAKCOMMON_OK);
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "TransientKey", -1), -1);
|
||||
// Defaults survive the reset.
|
||||
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", -1),
|
||||
1920);
|
||||
}
|
||||
|
||||
// --- error handler ----------------------------------------------------------
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct HandlerLog {
|
||||
int calls = 0;
|
||||
std::string title;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
void recording_handler(const char *title, const char *message,
|
||||
void *userdata)
|
||||
{
|
||||
auto *log = static_cast<HandlerLog *>(userdata);
|
||||
log->calls++;
|
||||
log->title = title;
|
||||
log->message = message;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(ConfigTest, ErrorHandlerFiresOnSaveFailure)
|
||||
{
|
||||
HandlerLog log;
|
||||
ASSERT_EQ(
|
||||
oakcommon_config_set_error_handler(recording_handler, &log),
|
||||
OAKCOMMON_OK);
|
||||
|
||||
// Point the config dir at a path that exists as a *file*: the temp
|
||||
// file cannot be created inside it, so save fails.
|
||||
const fs::path blocker = fs::temp_directory_path() /
|
||||
"oakconfig_test_blocker";
|
||||
{
|
||||
std::ofstream out(blocker.string());
|
||||
out << "not a directory";
|
||||
}
|
||||
setenv("OAK_CONFIG_DIR", blocker.string().c_str(), 1);
|
||||
|
||||
EXPECT_EQ(oakcommon_config_save(), OAKCOMMON_E_FAILED);
|
||||
EXPECT_EQ(log.calls, 1);
|
||||
EXPECT_FALSE(log.title.empty());
|
||||
EXPECT_FALSE(log.message.empty());
|
||||
|
||||
// Restore the per-test dir for TearDown.
|
||||
setenv("OAK_CONFIG_DIR", dir_.string().c_str(), 1);
|
||||
std::error_code ec;
|
||||
fs::remove(blocker, ec);
|
||||
}
|
||||
|
||||
TEST_F(ConfigTest, ClearErrorHandlerRestoresSilence)
|
||||
{
|
||||
// Registering and clearing must both succeed.
|
||||
ASSERT_EQ(
|
||||
oakcommon_config_set_error_handler(recording_handler, nullptr),
|
||||
OAKCOMMON_OK);
|
||||
EXPECT_EQ(oakcommon_config_set_error_handler(nullptr, nullptr),
|
||||
OAKCOMMON_OK);
|
||||
}
|
||||
|
||||
// --- entry_type error paths --------------------------------------------------
|
||||
|
||||
TEST_F(ConfigTest, EntryTypeErrorPaths)
|
||||
{
|
||||
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "NoSuchKey"),
|
||||
OAKCOMMON_E_NOT_FOUND);
|
||||
EXPECT_EQ(oakcommon_config_entry_type(nullptr, nullptr),
|
||||
OAKCOMMON_E_INVALID);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user