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:
2026-08-06 20:53:07 +08:00
parent 3d004c081b
commit 354df2e194
71 changed files with 8142 additions and 101 deletions
+1
View File
@@ -1,5 +1,6 @@
target_sources(oakcommon PRIVATE
colortransform.cpp
config.cpp
commandlineparser.cpp
current.cpp
subtitleparams.cpp
+314
View File
@@ -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;
}
}
+2
View File
@@ -19,6 +19,8 @@ add_library(oakcommon SHARED
commandlineparser.cpp
commandlineparser.h
colortransform.h
configstore.cpp
configstore.h
current.cpp
current.h
debug.cpp
+406
View File
@@ -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 &section : 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;
}
+132
View File
@@ -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 &current();
/**
* @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
+1
View File
@@ -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
+384
View File
@@ -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