eis/eqpalg/test/test_algorithms.cc

917 lines
30 KiB
C++
Raw Permalink Normal View History

// eqpalg/test/test_algorithms.cc
// 核心算法单元测试 — LogicAlg/BoundAlg/BoundHoldAlg/FeedbackAlg/ExpTimes/FaultCode/Roller3
//
// 设计说明:
// 算法子类 (LogicAlg/BoundAlg/...) 的构造函数依赖 AlgBase::init()
// 而后者又依赖 CMemVar共享内存等运行时基础设施在开发机上可能不可用。
// 因此本测试文件不尝试完整构造算法对象,而是:
// 1. 测试已从算法中提取出的独立组件ExpressionEngine, BoundChecker, FbStateMachine
// 2. 用独立的辅助函数测试算法特有的核心逻辑bit 提取、tag 号解析、中位数计算等)
// 3. 通过组件组合模拟 doMonProc() 的控制流,验证集成行为
//
// 已提取组件在 eqpalg/test/ 下的其他文件中已有完整单元测试:
// - test_expression_engine.cc — ExpressionEngine
// - test_fb_state_machine.cc — FbStateMachine
// 本文件在这些已有覆盖的基础上增加算法级集成测试和算法特有逻辑的测试。
#include <eqpalg/test/test_harness.h>
#include <eqpalg/utility/expression_engine.h>
#include <eqpalg/utility/bound_checker.h>
#include <eqpalg/utility/fb_state_machine.h>
#include <mix_cc/json.h>
#include <algorithm>
#include <chrono>
#include <cctype>
#include <map>
#include <string>
#include <vector>
using json = mix_cc::json;
// ============================================================================
// 测试辅助:构建各算法类型的合法 JSON 配置
// ============================================================================
// 构建 LogicAlg (ExpType::Logic = 1) 的最小合法 JSON
static json makeLogicConfig() {
return json::parse(R"({
"tags": {"tag1": {"value": "TEST_TAG"}},
"trigger": {"value": "100"},
"function": {
"result": {"value": "tag1 > 0.5"}
},
"output": {
"error": {"value": "逻辑异常"}
}
})");
}
// 构建 BoundAlg (ExpType::Bound = 2) 的最小合法 JSON
static json makeBoundConfig() {
return json::parse(R"({
"tags": {"tag1": {"value": "TEST_TAG"}},
"trigger": {"value": "100"},
"datasource": {"value": "1"},
"function": {
"result": {
"value": "tag1",
"param": {
"limit_down": {"value": "0"},
"limit_up": {"value": "100"},
"unit": {"value": "C"}
}
}
},
"output": {
"error": {"value": "温度超限"}
}
})");
}
// 构建 FeedbackAlg 逻辑型 (ExpType::FeedbackLogic = 3) 的最小合法 JSON
static json makeFeedbackLogicConfig() {
return json::parse(R"({
"tags": {"tag1": {"value": "TEST_TAG"}},
"trigger": {"value": "100"},
"function": {
"action_start": {"value": "tag1 > 0.5"},
"action_end": {
"value": "tag1 < 0.1",
"param": {
"timeout": {"value": "60000"},
"hold": {"value": "0"}
}
},
"result": {"value": "1"}
},
"output": {
"error": {"value": "反馈动作报警"}
}
})");
}
// 构建 FeedbackAlg 上下限型 (ExpType::FeedbackBound = 4) 的最小合法 JSON
static json makeFeedbackBoundConfig() {
return json::parse(R"({
"tags": {"tag1": {"value": "TEST_TAG"}},
"trigger": {"value": "100"},
"datasource": {"value": "1"},
"function": {
"action_start": {
"value": "tag1",
"param": {
"limit_down": {"value": "0"},
"limit_up": {"value": "100"},
"unit": {"value": "C"}
}
},
"action_end": {
"value": "tag1 < 50",
"param": {
"timeout": {"value": "60000"},
"hold": {"value": "0"}
}
},
"result": {"value": "1"}
},
"output": {
"error": {"value": "温度反馈报警"}
}
})");
}
// 构建 BoundHoldAlg (ExpType::BoundHold = 5) 的最小合法 JSON
static json makeBoundHoldConfig() {
return json::parse(R"({
"tags": {"tag1": {"value": "TEST_TAG"}},
"trigger": {"value": "100"},
"datasource": {"value": "1"},
"function": {
"result": {
"value": "tag1",
"param": {
"limit_down": {"value": "0"},
"limit_up": {"value": "100"},
"unit": {"value": "C"},
"hold_time": {"value": "5000"}
}
}
},
"output": {
"error": {"value": "温度持续超限"}
}
})");
}
// ============================================================================
// 表达式测试辅助环境(与 test_expression_engine.cc 一致)
// ============================================================================
struct TestEnv {
std::map<std::string, double> vars;
std::vector<std::string> tags;
ExpressionEngine engine;
TestEnv() : engine(vars, tags) {
tags = {"tag1", "tag2"};
vars["tag1"] = 10.0;
vars["tag2"] = 20.0;
vars["p1"] = 10.0;
vars["p2"] = 20.0;
vars["now"] = 0;
vars["stime"] = 0;
vars["time"] = 0;
vars["etime"] = 0;
}
};
// ============================================================================
// 第一部分LogicAlg 核心逻辑测试
// ============================================================================
// LogicAlg::doMonProc() 的核心逻辑:
// 1. exp_act_->evaluate() → bool (trigger)
// 2. trigger==true → alarm
// 3. trigger==false → 无 alarm
// 下面通过 ExpressionEngine 模拟此流程。
TEST(logic_alg_trigger_true_produces_alarm) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
// trigger 为 true → 应产生报警
bool triggered = engine.evaluateBool("act");
CHECK_EQ(triggered, true);
}
TEST(logic_alg_trigger_false_no_alarm) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 0.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
bool triggered = engine.evaluateBool("act");
CHECK_EQ(triggered, false);
}
TEST(logic_alg_complex_expression) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1", "tag2"};
vars["tag1"] = 3.0;
vars["tag2"] = 7.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 2 && tag2 < 10");
CHECK_EQ(engine.evaluateBool("act"), true);
}
TEST(logic_alg_and_short_circuit_false) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1", "tag2"};
vars["tag1"] = 0.0;
vars["tag2"] = 7.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 2 && tag2 < 10");
CHECK_EQ(engine.evaluateBool("act"), false);
}
TEST(logic_alg_or_expression) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1", "tag2"};
vars["tag1"] = 0.0;
vars["tag2"] = 100.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 2 || tag2 > 50");
CHECK_EQ(engine.evaluateBool("act"), true);
}
TEST(logic_alg_negation_operator) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 0.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "!tag1");
CHECK_EQ(engine.evaluateBool("act"), true);
}
// ============================================================================
// 第二部分BoundAlg — BoundChecker 检测器测试
// ============================================================================
// BoundAlg::doMonProc() 使用 BoundChecker 判断 value 是否超出 [down, up]。
// 哨兵值 -32768 表示"无下限"32767/32768 表示"无上限"。
TEST(bound_checker_detects_out_of_upper) {
BoundChecker bc;
bc.setLimits(0.0, 100.0);
CHECK_EQ(bc.isOutOfBounds(150.0), true);
CHECK_EQ(bc.isOutOfBounds(50.0), false);
}
TEST(bound_checker_detects_out_of_lower) {
BoundChecker bc;
bc.setLimits(10.0, 100.0);
CHECK_EQ(bc.isOutOfBounds(5.0), true);
}
TEST(bound_checker_only_right_mode_sentinel) {
BoundChecker bc;
bc.setLimits(-32768.0, 100.0); // -32768 = no lower bound
CHECK(bc.detectMode() == DetectMode::OnlyRight);
CHECK_EQ(bc.isOutOfBounds(-999.0), false); // below lower is OK
CHECK_EQ(bc.isOutOfBounds(150.0), true); // above upper triggers
}
TEST(bound_checker_only_left_mode_sentinel) {
BoundChecker bc;
bc.setLimits(10.0, 32767.0); // 32767 = no upper bound
CHECK(bc.detectMode() == DetectMode::OnlyLeft);
CHECK_EQ(bc.isOutOfBounds(5.0), true); // below lower triggers
CHECK_EQ(bc.isOutOfBounds(999.0), false); // above upper is OK (sentinel)
}
TEST(bound_checker_only_left_mode_sentinel_32768) {
BoundChecker bc;
bc.setLimits(10.0, 32768.0); // 32768 also = no upper bound
CHECK(bc.detectMode() == DetectMode::OnlyLeft);
CHECK_EQ(bc.isOutOfBounds(5.0), true);
CHECK_EQ(bc.isOutOfBounds(999.0), false);
}
TEST(bound_checker_default_bilateral) {
BoundChecker bc;
bc.setLimits(10.0, 20.0);
CHECK(bc.detectMode() == DetectMode::Default);
CHECK_EQ(bc.isOutOfBounds(5.0), true);
CHECK_EQ(bc.isOutOfBounds(15.0), false);
CHECK_EQ(bc.isOutOfBounds(25.0), true);
}
TEST(bound_checker_error_mode) {
BoundChecker bc;
bc.setLimits(-32768.0, -32768.0); // both sentinels
CHECK(bc.detectMode() == DetectMode::ErrorMode);
CHECK_EQ(bc.isOutOfBounds(50.0), false); // error mode: never alarm
}
TEST(bound_checker_exact_boundary_default) {
BoundChecker bc;
bc.setLimits(10.0, 20.0);
CHECK_EQ(bc.isOutOfBounds(10.0), false); // exactly on lower bound
CHECK_EQ(bc.isOutOfBounds(20.0), false); // exactly on upper bound
CHECK_EQ(bc.isOutOfBounds(10.00001), false);
}
TEST(bound_checker_boundary_only_left) {
BoundChecker bc;
bc.setLimits(10.0, 32767.0);
CHECK_EQ(bc.isOutOfBounds(10.0), false);
CHECK_EQ(bc.isOutOfBounds(9.999), true);
}
TEST(bound_checker_boundary_only_right) {
BoundChecker bc;
bc.setLimits(-32768.0, 100.0);
CHECK_EQ(bc.isOutOfBounds(100.0), false);
CHECK_EQ(bc.isOutOfBounds(100.001), true);
}
TEST(bound_checker_isValid) {
BoundChecker bc1;
bc1.setLimits(0.0, 100.0);
CHECK_EQ(bc1.isValid(), true);
BoundChecker bc2;
bc2.setLimits(-32768.0, -32768.0);
CHECK_EQ(bc2.isValid(), false);
}
TEST(bound_checker_setDetectMode_override) {
BoundChecker bc;
bc.setLimits(-32768.0, 100.0);
CHECK(bc.detectMode() == DetectMode::OnlyRight);
// manual override to Default, sentinel -32768 becomes real lower bound
bc.setDetectMode(DetectMode::Default);
CHECK(bc.detectMode() == DetectMode::Default);
// -999 > -32768 → within bounds; 150 > 100 → out of upper
CHECK_EQ(bc.isOutOfBounds(-999.0), false);
CHECK_EQ(bc.isOutOfBounds(150.0), true);
}
// ============================================================================
// 第三部分BoundAlg 过滤表达式测试
// ============================================================================
// BoundAlg::checkFilter() 使用 exp_feedback_ 作为过滤条件,
// 仅当条件为 true 时才进入统计累积。
TEST(bound_alg_filter_expression) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 150.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1");
engine.registerExpression("feedback", "tag1 > 200"); // filter
// tag1=150 → filter is false → no statistics
bool filter_ok = engine.evaluateBool("feedback");
CHECK_EQ(filter_ok, false);
// tag1=250 → filter is true
vars["tag1"] = 250.0;
filter_ok = engine.evaluateBool("feedback");
CHECK_EQ(filter_ok, true);
}
TEST(bound_alg_filter_boundary) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 200.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("feedback", "tag1 >= 200");
// exactly at boundary
CHECK_EQ(engine.evaluateBool("feedback"), true);
vars["tag1"] = 199.999;
CHECK_EQ(engine.evaluateBool("feedback"), false);
}
// ============================================================================
// 第四部分BoundHoldAlg 保持时间逻辑测试
// ============================================================================
// BoundHoldAlg::doMonProc() 要求 value 持续超出限值 hold_time 后才报警。
TEST(bound_hold_time_logic) {
double hold_time_ms = 5000.0; // 5 second hold
auto start = std::chrono::system_clock::now();
// 首次检测 — 尚未到达保持时间
auto now1 = start;
auto elapsed1 = std::chrono::duration_cast<std::chrono::milliseconds>(now1 - start);
CHECK(elapsed1 < std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
// 达到保持时间后 — 可以报警
auto now2 = start + std::chrono::milliseconds(static_cast<int>(hold_time_ms) + 100);
auto elapsed2 = std::chrono::duration_cast<std::chrono::milliseconds>(now2 - start);
CHECK(elapsed2 > std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
// hold_time <= delay_time → 立即报警
bool alarm_with_short = (std::chrono::milliseconds(50) <= std::chrono::milliseconds(50)) ||
(elapsed1 > std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
CHECK_EQ(alarm_with_short, true);
// Long hold — should alarm after time
bool alarm_after_hold = elapsed2 > std::chrono::milliseconds(static_cast<int>(hold_time_ms));
CHECK_EQ(alarm_after_hold, true);
}
TEST(bound_hold_zero_hold_time) {
double hold_time_ms = 0.0; // no hold → immediate alarm
auto start = std::chrono::system_clock::now();
auto now = start + std::chrono::milliseconds(1);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - start);
// hold_time == 0 → always alarm immediately
CHECK(elapsed >= std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
}
TEST(bound_hold_long_delay) {
double hold_time_ms = 60000.0; // 60 seconds
auto start = std::chrono::system_clock::now();
// before hold time expires
auto now1 = start + std::chrono::milliseconds(30000);
auto elapsed1 = std::chrono::duration_cast<std::chrono::milliseconds>(now1 - start);
CHECK(elapsed1 < std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
// after hold time expires
auto now2 = start + std::chrono::milliseconds(61000);
auto elapsed2 = std::chrono::duration_cast<std::chrono::milliseconds>(now2 - start);
CHECK(elapsed2 > std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
}
// ============================================================================
// 第五部分FeedbackAlg / FbStateMachine 集成测试
// ============================================================================
// FbStateMachine 的完整单元测试在 test_fb_state_machine.cc 中。
// 本部分测试 FeedbackAlg::doMonProc() 的整体流程 ——
// ExpressionEngine (exp_act_ + exp_feedback_) + FbStateMachine 的组合行为。
TEST(feedback_full_flow_start_to_done) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
vars["p1"] = 1.0;
vars["now"] = 0;
vars["stime"] = 0;
vars["time"] = 0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
engine.registerExpression("feedback", "tag1 < 0.1");
engine.registerExpression("result", "1");
FbStateMachine fsm;
fsm.configure(false, std::chrono::milliseconds(60000));
auto now = std::chrono::system_clock::now();
vars["now"] = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
// Step 1: Trigger → Started
bool triggered = engine.evaluateBool("act");
CHECK_EQ(triggered, true);
auto [state1, reset1] = fsm.update(triggered, now, vars, tags.size());
CHECK(state1 == FbState::Started);
CHECK_EQ(reset1, false);
// Step 2: Continue → InProgress (may take 2 cycles from Started)
auto [state2, reset2] = fsm.update(true, now, vars, tags.size());
if (state2 == FbState::Started) {
// need one more cycle
auto [state2b, reset2b] = fsm.update(true, now, vars, tags.size());
CHECK(state2b == FbState::InProgress || state2b == FbState::Started);
state2 = state2b;
(void)reset2b;
}
CHECK(state2 == FbState::InProgress);
CHECK_EQ(reset2, false);
}
TEST(feedback_full_flow_not_hold) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
vars["p1"] = 1.0;
vars["now"] = 0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
FbStateMachine fsm;
fsm.configure(true, std::chrono::milliseconds(60000)); // keep_mode=true
auto now = std::chrono::system_clock::now();
vars["now"] = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
// Start the action (two cycles to reach InProgress)
fsm.update(true, now, vars, tags.size());
auto state2 = fsm.update(true, now, vars, tags.size()).state;
if (state2 == FbState::Started) {
state2 = fsm.update(true, now, vars, tags.size()).state;
}
// Lose trigger with keep_mode → NotHold
auto [state, reset] = fsm.update(false, now, vars, tags.size());
if (state == FbState::NotHold) {
CHECK_EQ(reset, true);
}
// InProgress is also valid if keep_mode transition hasn't triggered yet
}
TEST(feedback_timeout_scenario) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
vars["p1"] = 1.0;
vars["now"] = 0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
FbStateMachine fsm;
fsm.configure(false, std::chrono::milliseconds(100)); // short 100ms timeout
auto now = std::chrono::system_clock::now();
vars["now"] = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
// Trigger → Started → InProgress
fsm.update(true, now, vars, tags.size());
auto state = fsm.update(true, now, vars, tags.size()).state;
if (state == FbState::Started) {
state = fsm.update(true, now, vars, tags.size()).state;
}
CHECK(state == FbState::InProgress || state == FbState::Timeout);
// Advance past timeout
now += std::chrono::milliseconds(200);
vars["now"] = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
auto result = fsm.update(true, now, vars, tags.size());
CHECK(result.state == FbState::Timeout);
CHECK_EQ(result.funVarsNeedReset, true);
}
// ============================================================================
// 第六部分ExpTimes 累积逻辑测试
// ============================================================================
// ExpTimes::update_times() 在 exp_act_ 为 true 时累加次数/时间。
TEST(exp_times_occurrence_counting_logic) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1");
int shear_times = 0;
int max_times = 5;
// 模拟 6 个周期,每个周期 exp_act_ 为 true
for (int i = 0; i < 6; i++) {
if (engine.evaluateBool("act")) {
shear_times++;
}
}
CHECK_EQ(shear_times, 6);
CHECK_EQ(shear_times > max_times, true); // 应报警
}
TEST(exp_times_occurrence_below_threshold) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
int shear_times = 0;
int max_times = 5;
for (int i = 0; i < 3; i++) {
if (engine.evaluateBool("act")) {
shear_times++;
}
}
CHECK_EQ(shear_times, 3);
CHECK_EQ(shear_times > max_times, false); // under threshold, no alarm
}
TEST(exp_times_time_accumulation_logic) {
// 模拟时间累积:触发→累积时间→取消触发→停止
bool act_started = false;
double running_time = 0.0;
auto start_time = std::chrono::system_clock::now();
// 触发变为 true
act_started = true;
auto t1 = std::chrono::system_clock::now();
// 时间推进
auto t2 = t1 + std::chrono::milliseconds(5000);
double elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
running_time += elapsed / (60.0 * 60000); // ms → hours
// 触发变为 false
act_started = false;
CHECK_FLOAT_EQ(running_time, 5000.0 / 3600000.0, 0.0001);
CHECK(running_time > 0.0);
}
TEST(exp_times_time_multiple_intervals) {
double running_time = 0.0;
auto t0 = std::chrono::system_clock::now();
// Interval 1: 3000ms
auto t1 = t0 + std::chrono::milliseconds(3000);
running_time += std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count()
/ (60.0 * 60000);
// Interval 2: 7000ms
auto t2 = t1 + std::chrono::milliseconds(7000);
running_time += std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
/ (60.0 * 60000);
double expected = 10000.0 / 3600000.0;
CHECK_FLOAT_EQ(running_time, expected, 0.0001);
}
// ============================================================================
// 第七部分FaultCode 位提取 (gitbit) 测试
// ============================================================================
// FaultCode::exec_mon() 使用 gitbit 模板函数解析故障代码的各个 bit。
template <typename T>
static T gitbit(T value, T sub) {
return int(value) >> int(sub) & 1;
}
TEST(fault_code_gitbit_extraction) {
// Bit 3 of 0b00001000 = 8
CHECK_EQ(gitbit(8, 3), 1);
CHECK_EQ(gitbit(8, 0), 0);
CHECK_EQ(gitbit(8, 1), 0);
CHECK_EQ(gitbit(8, 2), 0);
// Bit 0 of 1
CHECK_EQ(gitbit(1, 0), 1);
// Complex code: 0b1010 = 10
CHECK_EQ(gitbit(10, 0), 0); // bit 0
CHECK_EQ(gitbit(10, 1), 1); // bit 1
CHECK_EQ(gitbit(10, 2), 0); // bit 2
CHECK_EQ(gitbit(10, 3), 1); // bit 3
// All bits 0-15 for code=0
for (int i = 0; i < 16; i++) {
CHECK_EQ(gitbit(0, i), 0);
}
}
TEST(fault_code_bit_parsing_all_bits) {
// A code with multiple bits set: 0b1111 = 15
int code = 15;
int set_bits = 0;
for (int i = 0; i < 16; i++) {
if (gitbit(code, i)) set_bits++;
}
CHECK_EQ(set_bits, 4); // bits 0,1,2,3
}
TEST(fault_code_bit_15_set) {
// A code with bit 15 set: 0x8000 = 32768
int code = 32768;
CHECK_EQ(gitbit(code, 15), 1);
CHECK_EQ(gitbit(code, 0), 0);
CHECK_EQ(gitbit(code, 7), 0);
}
TEST(fault_code_all_bits_set_16bit) {
// 0xFFFF = 65535, all lower 16 bits set
int code = 65535;
int set_bits = 0;
for (int i = 0; i < 16; i++) {
if (gitbit(code, i)) set_bits++;
}
CHECK_EQ(set_bits, 16);
}
TEST(fault_code_single_bit_each_position) {
// Verify each individual bit position
for (int i = 0; i < 16; i++) {
int code = 1 << i;
CHECK_EQ(gitbit(code, i), 1);
// all other bits should be 0
for (int j = 0; j < 16; j++) {
if (j != i) {
CHECK_EQ(gitbit(code, j), 0);
}
}
}
}
TEST(fault_code_gitbit_with_different_types) {
// Test with unsigned types (FaultCode handles various integer types)
CHECK_EQ(gitbit<unsigned int>(8u, 3u), 1u);
CHECK_EQ(gitbit<unsigned int>(8u, 0u), 0u);
CHECK_EQ(gitbit<short>(1, 0), 1);
CHECK_EQ(gitbit<long>(32768L, 15L), 1L);
}
// ============================================================================
// 第八部分Roller3 辅助函数测试
// ============================================================================
// extractTagNumbers — 从表达式中提取 "tagN" 的数字序号
// calculateMedian — 计算数组的中位数
static std::vector<int> extractTagNumbers(const std::string& expr) {
std::vector<int> result;
const std::string tagPrefix = "tag";
size_t pos = 0;
while (pos < expr.size()) {
size_t found = expr.find(tagPrefix, pos);
if (found == std::string::npos) break;
size_t numStart = found + tagPrefix.size();
size_t numEnd = numStart;
while (numEnd < expr.size() && isdigit(expr[numEnd])) numEnd++;
if (numEnd > numStart) {
std::string numStr = expr.substr(numStart, numEnd - numStart);
result.push_back(std::stoi(numStr));
}
pos = numEnd;
}
return result;
}
static double calculateMedian(std::vector<double> data) {
if (data.empty()) return 0.0;
size_t size = data.size();
std::sort(data.begin(), data.end());
if (size % 2 == 0) {
return (data[size / 2 - 1] + data[size / 2]) / 2.0;
} else {
return data[size / 2];
}
}
TEST(roller3_extract_tag_numbers) {
auto tags = extractTagNumbers("tag1+tag2+tag3");
CHECK_EQ(tags.size(), 3u);
CHECK_EQ(tags[0], 1);
CHECK_EQ(tags[1], 2);
CHECK_EQ(tags[2], 3);
}
TEST(roller3_extract_tag_numbers_single) {
auto tags = extractTagNumbers("tag5");
CHECK_EQ(tags.size(), 1u);
CHECK_EQ(tags[0], 5);
}
TEST(roller3_extract_tag_numbers_complex) {
auto tags = extractTagNumbers("tag10+tag2+tag35");
CHECK_EQ(tags.size(), 3u);
bool has10 = std::find(tags.begin(), tags.end(), 10) != tags.end();
bool has2 = std::find(tags.begin(), tags.end(), 2) != tags.end();
bool has35 = std::find(tags.begin(), tags.end(), 35) != tags.end();
CHECK_EQ(has10, true);
CHECK_EQ(has2, true);
CHECK_EQ(has35, true);
}
TEST(roller3_extract_empty_string) {
auto tags = extractTagNumbers("");
CHECK_EQ(tags.size(), 0u);
}
TEST(roller3_extract_no_tags) {
auto tags = extractTagNumbers("no_tags_here");
CHECK_EQ(tags.size(), 0u);
}
TEST(roller3_extract_tag_numbers_with_prefix) {
// "tag" embedded in other words should still be found
auto tags = extractTagNumbers("metatag1+tag2");
// extractTagNumbers searches for "tag" substring, so "metatag1" → "tag1" → 1
CHECK_EQ(tags.size(), 2u);
bool has1 = std::find(tags.begin(), tags.end(), 1) != tags.end();
bool has2 = std::find(tags.begin(), tags.end(), 2) != tags.end();
CHECK_EQ(has1, true);
CHECK_EQ(has2, true);
}
TEST(roller3_extract_large_tag_numbers) {
auto tags = extractTagNumbers("tag999+tag1000");
CHECK_EQ(tags.size(), 2u);
CHECK_EQ(tags[0], 999);
CHECK_EQ(tags[1], 1000);
}
TEST(roller3_calculate_median_odd) {
CHECK_FLOAT_EQ(calculateMedian({1.0, 3.0, 5.0}), 3.0, 0.001);
CHECK_FLOAT_EQ(calculateMedian({10.0, 2.0, 8.0}), 8.0, 0.001);
}
TEST(roller3_calculate_median_even) {
CHECK_FLOAT_EQ(calculateMedian({1.0, 3.0, 5.0, 7.0}), 4.0, 0.001);
CHECK_FLOAT_EQ(calculateMedian({1.0, 2.0}), 1.5, 0.001);
}
TEST(roller3_calculate_median_single) {
CHECK_FLOAT_EQ(calculateMedian({42.0}), 42.0, 0.001);
}
TEST(roller3_calculate_median_empty) {
CHECK_FLOAT_EQ(calculateMedian({}), 0.0, 0.001);
}
TEST(roller3_calculate_median_negative_values) {
CHECK_FLOAT_EQ(calculateMedian({-5.0, -1.0, -3.0}), -3.0, 0.001);
CHECK_FLOAT_EQ(calculateMedian({-10.0, 0.0, 10.0, 20.0}), 5.0, 0.001);
}
TEST(roller3_calculate_median_large_dataset) {
std::vector<double> data;
for (int i = 1; i <= 99; i++) {
data.push_back(static_cast<double>(i));
}
CHECK_FLOAT_EQ(calculateMedian(data), 50.0, 0.001);
}
TEST(roller3_calculate_median_unsorted_preserves_order) {
// verify the function sorts internally
std::vector<double> data = {5.0, 3.0, 1.0, 4.0, 2.0};
CHECK_FLOAT_EQ(calculateMedian(data), 3.0, 0.001);
}
// ============================================================================
// JSON 配置校验
// ============================================================================
// 验证辅助函数构造的 JSON 结构是否合法且包含必要字段。
TEST(json_config_logic_alg_structure) {
json cfg = makeLogicConfig();
CHECK(cfg.contains("tags"));
CHECK(cfg.contains("trigger"));
CHECK(cfg.contains("function"));
CHECK(cfg.contains("output"));
CHECK(cfg.at("function").contains("result"));
CHECK(cfg.at("function").at("result").contains("value"));
CHECK(cfg.at("output").contains("error"));
}
TEST(json_config_bound_alg_structure) {
json cfg = makeBoundConfig();
CHECK(cfg.contains("tags"));
CHECK(cfg.contains("function"));
CHECK(cfg.at("function").contains("result"));
CHECK(cfg.at("function").at("result").contains("param"));
auto& param = cfg.at("function").at("result").at("param");
CHECK(param.contains("limit_down"));
CHECK(param.contains("limit_up"));
}
TEST(json_config_feedback_logic_structure) {
json cfg = makeFeedbackLogicConfig();
CHECK(cfg.contains("function"));
CHECK(cfg.at("function").contains("action_start"));
CHECK(cfg.at("function").contains("action_end"));
// action_end should have param with timeout
CHECK(cfg.at("function").at("action_end").contains("param"));
CHECK(cfg.at("function").at("action_end").at("param").contains("timeout"));
}
TEST(json_config_feedback_bound_structure) {
json cfg = makeFeedbackBoundConfig();
CHECK(cfg.at("function").contains("action_start"));
CHECK(cfg.at("function").contains("action_end"));
// action_start should have bound params
auto& start = cfg.at("function").at("action_start");
CHECK(start.contains("param"));
CHECK(start.at("param").contains("limit_down"));
CHECK(start.at("param").contains("limit_up"));
}
TEST(json_config_bound_hold_structure) {
json cfg = makeBoundHoldConfig();
auto& param = cfg.at("function").at("result").at("param");
CHECK(param.contains("hold_time"));
CHECK(param.contains("limit_down"));
CHECK(param.contains("limit_up"));
}