为 RNG 模块添加 CTest 单元测试,覆盖常量/线性/漂移/正弦/复合/布尔/注册表/阀对等模型

This commit is contained in:
Huamonarch 2026-05-15 09:14:02 +08:00
parent b99cd0a73c
commit 65863b326f
12 changed files with 509 additions and 17 deletions

View File

@ -56,23 +56,29 @@ target_include_directories(
set_target_properties(RNG PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${bin_dir}) set_target_properties(RNG PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${bin_dir})
# ###################### add test ######################## # ###################### add test ########################
# 1. loss_compress_test include(CTest)
#
# ##############################################################################
# include(../cmake_include/unit_test.cmake)
# add_executable(loss_compress_test ${DISTRIBUTION} test/loss_compress_test.cc) aux_source_directory(./test TEST_SOURCES)
# target_link_libraries(loss_compress_test ${LINK_OPTION}
# Boost::unit_test_framework)
# target_include_directories(
# loss_compress_test PUBLIC ./ ../ ../../inc ../../inc/dbinc
# ${iPlature_include})
# set_target_properties(loss_compress_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY add_executable(rng_test
# ${UNIT_TEST_BIN_OUTPUT_DIR}) ${TEST_SOURCES}
./model/ModelRegistry.cc
)
# enable_testing() target_compile_definitions(rng_test PRIVATE
# add_test( TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/test"
# NAME loss_compress_test )
# WORKING_DIRECTORY ${UNIT_TEST_BIN_OUTPUT_DIR}
# COMMAND loss_compress_test) target_include_directories(rng_test PUBLIC
./
../
${my_lib_include}
)
set_target_properties(rng_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/test)
enable_testing()
add_test(NAME rng_test
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/test
COMMAND rng_test
)

View File

@ -0,0 +1,76 @@
#include "test_harness.h"
#include <TestProject/RNG/model/BoolToggleModel.h>
#include <TestProject/RNG/model/BoolRandomModel.h>
#include <TestProject/RNG/model/NotModel.h>
#include <TestProject/RNG/model/ConstantModel.h>
using json = nlohmann::json;
// --- BoolToggleModel ---
TEST(toggle_default_period) {
// default period_ms=2000 → 100 ticks per period, first 50 true
BoolToggleModel m(json::object(), 0.0f);
CHECK_EQ(m.evaluateBool(0), true);
CHECK_EQ(m.evaluateBool(49), true);
CHECK_EQ(m.evaluateBool(50), false);
CHECK_EQ(m.evaluateBool(99), false);
CHECK_EQ(m.evaluateBool(100), true); // new period
}
TEST(toggle_custom_period) {
// period_ms=40 → 2 ticks per period, first 1 true
BoolToggleModel m(json{{"period_ms", 40}}, 0.0f);
CHECK_EQ(m.evaluateBool(0), true);
CHECK_EQ(m.evaluateBool(1), false);
CHECK_EQ(m.evaluateBool(2), true);
}
TEST(toggle_large_t) {
BoolToggleModel m(json {{"period_ms", 2000}}, 0.0f);
// After 5000 periods, behavior should hold
size_t base = 5000 * 100;
CHECK_EQ(m.evaluateBool(base), true);
CHECK_EQ(m.evaluateBool(base + 50), false);
}
// --- BoolRandomModel ---
TEST(random_always_true) {
BoolRandomModel m(json{{"prob_true", 1.0f}}, 0.0f);
for (int i = 0; i < 100; i++) {
CHECK_EQ(m.evaluateBool(i), true);
}
}
TEST(random_always_false) {
BoolRandomModel m(json{{"prob_true", 0.0f}}, 0.0f);
for (int i = 0; i < 100; i++) {
CHECK_EQ(m.evaluateBool(i), false);
}
}
TEST(random_default_prob_is_half) {
BoolRandomModel m(json::object(), 0.0f);
int trues = 0;
for (int i = 0; i < 10000; i++) {
if (m.evaluateBool(i)) trues++;
}
// With 10000 samples, true rate should be in [0.35, 0.65]
CHECK(trues > 3500);
CHECK(trues < 6500);
}
// --- NotModel ---
TEST(not_inverts) {
auto inner = std::make_unique<BoolToggleModel>(json::object(), 0.0f);
NotModel m(std::move(inner));
CHECK_EQ(m.evaluateBool(0), false); // toggle is true at t=0
CHECK_EQ(m.evaluateBool(50), true); // toggle is false at t=50
}
TEST(not_evaluate_returns_zero) {
auto inner = std::make_unique<ConstantModel>(json::object(), 5.0f);
NotModel m(std::move(inner));
CHECK_FLOAT_EQ(m.evaluate(0), 0.0f, 0.001f);
}

View File

@ -0,0 +1,28 @@
#include "test_harness.h"
#include <TestProject/RNG/model/CompositeModel.h>
#include <TestProject/RNG/model/ConstantModel.h>
#include <TestProject/RNG/model/LinearModel.h>
using json = nlohmann::json;
TEST(composite_sums_base_and_noise) {
auto base = std::make_unique<ConstantModel>(json::object(), 50.0f);
auto noise = std::make_unique<LinearModel>(json{{"k", 2.0f}}, 0.0f);
CompositeModel m(std::move(base), std::move(noise));
CHECK_FLOAT_EQ(m.evaluate(0), 50.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(5), 60.0f, 0.001f);
}
TEST(composite_both_constant) {
auto base = std::make_unique<ConstantModel>(json::object(), 10.0f);
auto noise = std::make_unique<ConstantModel>(json::object(), 3.0f);
CompositeModel m(std::move(base), std::move(noise));
CHECK_FLOAT_EQ(m.evaluate(0), 13.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(999), 13.0f, 0.001f);
}
TEST(composite_evaluateBool_returns_false) {
auto base = std::make_unique<ConstantModel>(json::object(), 1.0f);
auto noise = std::make_unique<ConstantModel>(json::object(), 2.0f);
CompositeModel m(std::move(base), std::move(noise));
CHECK_EQ(m.evaluateBool(0), false);
}

View File

@ -0,0 +1,25 @@
#include "test_harness.h"
#include <TestProject/RNG/model/ConstantModel.h>
using json = nlohmann::json;
TEST(constant_returns_default) {
ConstantModel m(json::object(), 100.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 100.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(999), 100.0f, 0.001f);
}
TEST(constant_zero) {
ConstantModel m(json::object(), 0.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 0.0f, 0.001f);
}
TEST(constant_negative) {
ConstantModel m(json::object(), -42.5f);
CHECK_FLOAT_EQ(m.evaluate(0), -42.5f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(1), -42.5f, 0.001f);
}
TEST(constant_evaluateBool_returns_false) {
ConstantModel m(json::object(), 1.0f);
CHECK_EQ(m.evaluateBool(0), false);
}

View File

@ -0,0 +1,28 @@
#include "test_harness.h"
#include <TestProject/RNG/model/DriftModel.h>
using json = nlohmann::json;
TEST(drift_basic) {
DriftModel m(json{{"drift_rate", 0.1f}}, 0.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 0.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(10), 1.0f, 0.001f);
}
TEST(drift_default_rate_is_zero) {
DriftModel m(json::object(), 42.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 42.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(1000), 42.0f, 0.001f);
}
TEST(drift_negative_rate) {
DriftModel m(json{{"drift_rate", -0.5f}}, 100.0f);
CHECK_FLOAT_EQ(m.evaluate(10), 95.0f, 0.001f);
}
TEST(drift_with_period) {
// period_ms=100 → 5 ticks per period
DriftModel m(json{{"drift_rate", 1.0f}, {"period_ms", 100}}, 0.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 0.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(4), 4.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(5), 0.0f, 0.001f); // wraps
}

View File

@ -0,0 +1,71 @@
#pragma once
#include <cmath>
#include <functional>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
struct TestRunner {
struct Case {
const char* name;
std::function<void()> fn;
};
inline static std::vector<Case> cases;
static int run() {
int passed = 0, failed = 0;
for (auto& c : cases) {
std::cout << " " << c.name << " ... ";
try {
c.fn();
std::cout << "PASSED\n";
passed++;
} catch (const std::exception& e) {
std::cout << "FAILED\n " << e.what() << "\n";
failed++;
} catch (...) {
std::cout << "FAILED (unknown)\n";
failed++;
}
}
std::cout << "\n" << passed << " passed, " << failed << " failed\n";
return failed ? 1 : 0;
}
};
struct AutoReg {
AutoReg(const char* name, std::function<void()> fn) {
TestRunner::cases.push_back({name, std::move(fn)});
}
};
#define TEST(name) \
static void testfn_##name(); \
static AutoReg autoreg_##name(#name, testfn_##name); \
static void testfn_##name()
#define CHECK(expr) \
do { \
if (!(expr)) \
throw std::runtime_error("CHECK(" #expr ") failed"); \
} while (0)
#define CHECK_EQ(a, b) \
do { \
if ((a) != (b)) { \
std::ostringstream os; \
os << "CHECK_EQ: " << (a) << " != " << (b); \
throw std::runtime_error(os.str()); \
} \
} while (0)
#define CHECK_FLOAT_EQ(a, b, eps) \
do { \
if (std::fabs((a) - (b)) > (eps)) { \
std::ostringstream os; \
os << "CHECK_FLOAT_EQ: |" << (a) << " - " << (b) << "| > " << (eps); \
throw std::runtime_error(os.str()); \
} \
} while (0)

View File

@ -0,0 +1,42 @@
#include "test_harness.h"
#include <TestProject/RNG/model/LinearModel.h>
using json = nlohmann::json;
TEST(linear_basic) {
LinearModel m(json{{"k", 2.0f}}, 0.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 0.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(1), 2.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(5), 10.0f, 0.001f);
}
TEST(linear_default_k_is_zero) {
LinearModel m(json::object(), 50.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 50.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(100), 50.0f, 0.001f);
}
TEST(linear_negative_k) {
LinearModel m(json{{"k", -1.0f}}, 10.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 10.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(3), 7.0f, 0.001f);
}
TEST(linear_with_period) {
// period_ms=100 → 5 ticks per period
LinearModel m(json{{"k", 1.0f}, {"period_ms", 100}}, 0.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 0.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(4), 4.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(5), 0.0f, 0.001f); // wraps
CHECK_FLOAT_EQ(m.evaluate(7), 2.0f, 0.001f); // 7%5=2
}
TEST(linear_no_period) {
LinearModel m(json{{"k", 0.5f}}, 100.0f);
CHECK_FLOAT_EQ(m.evaluate(10), 105.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(1000), 600.0f, 0.001f);
}
TEST(linear_large_values) {
LinearModel m(json{{"k", 0.001f}}, 0.0f);
CHECK_FLOAT_EQ(m.evaluate(100000), 100.0f, 0.01f);
}

View File

@ -0,0 +1,6 @@
#include "test_harness.h"
int main() {
std::cout << "RNG Model Tests\n===============\n\n";
return TestRunner::run();
}

View File

@ -0,0 +1,9 @@
{
"models": {
"const_100": { "mode": "constant", "params": {} },
"linear_k1": { "mode": "linear", "params": { "k": 1.0 } },
"toggle_1s": { "mode": "bool_toggle", "params": { "period_ms": 1000 } },
"valve_std": { "mode": "valve_pair", "params": { "on_delay_ms": 100, "off_delay_ms": 80 } },
"normal_tiny": { "mode": "normal", "params": { "sigma": 0.01 } }
}
}

View File

@ -0,0 +1,87 @@
#include "test_harness.h"
#include <TestProject/RNG/model/ModelRegistry.h>
using json = nlohmann::json;
static const char* testJsonPath() {
// TEST_DATA_DIR is defined via CMake target_compile_definitions
return TEST_DATA_DIR "/test_models.json";
}
TEST(registry_singleton_is_same) {
CHECK_EQ(&ModelRegistry::instance(), &ModelRegistry::instance());
}
TEST(registry_load_models_and_get_builtin) {
auto& reg = ModelRegistry::instance();
reg.loadModels(testJsonPath());
IModel* m = reg.getOrCreate("const_100", 0.0f, "builtin_1");
CHECK(m != nullptr);
CHECK_FLOAT_EQ(m->evaluate(0), 100.0f, 0.001f);
CHECK_FLOAT_EQ(m->evaluate(999), 100.0f, 0.001f);
}
TEST(registry_linear_model_via_template) {
auto& reg = ModelRegistry::instance();
IModel* m = reg.getOrCreate("linear_k1", 5.0f, "linear_1");
CHECK(m != nullptr);
CHECK_FLOAT_EQ(m->evaluate(0), 5.0f, 0.001f);
CHECK_FLOAT_EQ(m->evaluate(3), 8.0f, 0.001f);
}
TEST(registry_same_key_returns_same_instance) {
auto& reg = ModelRegistry::instance();
IModel* a = reg.getOrCreate("const_100", 10.0f, "same_key");
IModel* b = reg.getOrCreate("linear_k1", 20.0f, "same_key");
// same instanceKey → returns cached instance, not a new one
CHECK_EQ(a, b);
}
TEST(registry_different_keys_different_instances) {
auto& reg = ModelRegistry::instance();
IModel* a = reg.getOrCreate("const_100", 0.0f, "key_a");
IModel* b = reg.getOrCreate("const_100", 0.0f, "key_b");
CHECK(a != b);
}
TEST(registry_empty_spec_falls_back_to_normal_tiny) {
auto& reg = ModelRegistry::instance();
IModel* m = reg.getOrCreate("", 0.0f, "empty_spec_test");
CHECK(m != nullptr);
// normal_tiny → NormalModel with sigma=0.01, returns near-default
float v = m->evaluate(0);
(void)v;
}
TEST(registry_find_by_model_name) {
auto& reg = ModelRegistry::instance();
// "const_100" was created via getOrCreate above; should be tracked
auto found = reg.findByModelName("const_100");
CHECK(!found.empty());
}
TEST(registry_find_nonexistent) {
auto& reg = ModelRegistry::instance();
auto found = reg.findByModelName("no_such_model_xyz");
CHECK(found.empty());
}
TEST(registry_composite_syntax) {
auto& reg = ModelRegistry::instance();
// const_100 + linear_k1: evaluate(0) = 100 + 0 = 100, evaluate(3) = 100 + 8 = 108
IModel* m = reg.getOrCreate("const_100+linear_k1", 0.0f, "composite_1");
CHECK(m != nullptr);
CHECK_FLOAT_EQ(m->evaluate(0), 100.0f, 0.001f);
CHECK_FLOAT_EQ(m->evaluate(3), 108.0f, 0.001f);
}
TEST(registry_not_syntax) {
auto& reg = ModelRegistry::instance();
// !toggle_1s: negates the bool toggle
IModel* m = reg.getOrCreate("!toggle_1s", 0.0f, "not_1");
CHECK(m != nullptr);
// toggle_1s period_ms=1000 → 50 ticks, first 25 true.
// !toggle_1s at t=0 should be false
CHECK_EQ(m->evaluateBool(0), false);
CHECK_EQ(m->evaluateBool(26), true); // toggle is false at 26, so ! is true
}

View File

@ -0,0 +1,45 @@
#include "test_harness.h"
#include <TestProject/RNG/model/SineModel.h>
#include <cmath>
using json = nlohmann::json;
TEST(sine_at_zero) {
SineModel m(json::object(), 0.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 0.0f, 0.001f);
}
TEST(sine_basic) {
// A=1, omega=π/2, phi=0 → sin(π/2 * t)
float omega = M_PI / 2.0f;
SineModel m(json{{"A", 1.0f}, {"omega", omega}}, 0.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 0.0f, 0.001f); // sin(0)=0
CHECK_FLOAT_EQ(m.evaluate(1), 1.0f, 0.001f); // sin(π/2)=1
CHECK_FLOAT_EQ(m.evaluate(2), 0.0f, 0.01f); // sin(π)=0
CHECK_FLOAT_EQ(m.evaluate(3), -1.0f, 0.001f); // sin(3π/2)=-1
}
TEST(sine_with_offset) {
float omega = M_PI / 2.0f;
SineModel m(json{{"A", 1.0f}, {"omega", omega}}, 100.0f);
CHECK_FLOAT_EQ(m.evaluate(1), 101.0f, 0.001f); // sin(π/2)=1 + 100
CHECK_FLOAT_EQ(m.evaluate(3), 99.0f, 0.001f); // sin(3π/2)=-1 + 100
}
TEST(sine_zero_amplitude) {
SineModel m(json{{"A", 0.0f}, {"omega", 1.0f}}, 5.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 5.0f, 0.001f);
CHECK_FLOAT_EQ(m.evaluate(100), 5.0f, 0.001f);
}
TEST(sine_with_phase) {
// phi=π/2 → sin(ωt + π/2) = cos(ωt). At t=0: cos(0)=1
float phi = M_PI / 2.0f;
SineModel m(json{{"A", 1.0f}, {"omega", 0.0f}, {"phi", phi}}, 0.0f);
CHECK_FLOAT_EQ(m.evaluate(0), 1.0f, 0.001f);
}
TEST(sine_negative_amplitude) {
float omega = M_PI / 2.0f;
SineModel m(json{{"A", -2.0f}, {"omega", omega}}, 0.0f);
CHECK_FLOAT_EQ(m.evaluate(1), -2.0f, 0.001f);
}

View File

@ -0,0 +1,69 @@
#include "test_harness.h"
#include <TestProject/RNG/model/ValvePairModel.h>
#include <TestProject/RNG/model/BoolToggleModel.h>
using json = nlohmann::json;
// Helper: create a simple action model and attach it
struct ValvePairFixture {
std::unique_ptr<BoolToggleModel> action;
ValvePairModel valve;
ValvePairFixture(int on_delay_ms = 0, int off_delay_ms = 0)
: action(std::make_unique<BoolToggleModel>(json{{"period_ms", 200}}, 0.0f))
, valve(json{{"on_delay_ms", on_delay_ms}, {"off_delay_ms", off_delay_ms}}, 0.0f) {
valve.action = action.get();
}
};
TEST(valve_no_action_returns_false) {
ValvePairModel m(json::object(), 0.0f);
CHECK_EQ(m.evaluateBool(0), false);
}
TEST(valve_idle_to_high_direct) {
// Zero delays: action=true → immediately high
ValvePairFixture f(0, 0);
// At t=0, toggle is true (first half of period 100 ticks)
CHECK_EQ(f.valve.evaluateBool(0), true);
}
TEST(valve_falls_when_action_goes_false) {
// Zero off delay: when action becomes false, output immediately false
ValvePairFixture f(0, 0);
// t=0..49: action=true, output=true
CHECK_EQ(f.valve.evaluateBool(0), true);
// t=50..99: action=false, output=false (no delay)
for (int i = 0; i < 49; i++) f.valve.evaluateBool(i); // advance
CHECK_EQ(f.valve.evaluateBool(49), true);
// t=50: action goes false, check
CHECK_EQ(f.valve.evaluateBool(50), false);
}
TEST(valve_on_delay) {
// on_delay_ms=40 → 2 ticks delay before rising
ValvePairFixture f(40, 0);
// t=0: action becomes true, but valve should wait 2 ticks
bool t0 = f.valve.evaluateBool(0); // transitions to WAITING_RISE
bool t1 = f.valve.evaluateBool(1); // still waiting
bool t2 = f.valve.evaluateBool(2); // should be HIGH now
CHECK_EQ(f.valve.evaluateBool(3), true);
(void)t0; (void)t1; (void)t2;
// The exact tick when it becomes true depends on how the counter
// interacts with the evaluate calls. Just verify it eventually goes high.
// After delay_counter counts down from on_delay_ticks, it should be HIGH.
int ticks = (int)f.valve.on_delay_ticks + 1;
bool went_high = false;
for (int i = 0; i < ticks + 5; i++) {
if (f.valve.evaluateBool(i)) { went_high = true; break; }
}
CHECK(went_high);
}
TEST(valve_reset) {
ValvePairFixture f(100, 100); // long delays
f.valve.evaluateBool(0); // enter WAITING_RISE
f.valve.reset();
// After reset, should be IDLE_LOW
CHECK_EQ(f.valve.state, ValvePairModel::IDLE_LOW);
CHECK_EQ(f.valve.evaluateBool(0), false);
}