// 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 #include #include #include #include #include #include #include #include #include #include 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 vars; std::vector 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 vars; std::vector 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 vars; std::vector 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 vars; std::vector 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 vars; std::vector 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 vars; std::vector 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 vars; std::vector 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 vars; std::vector 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 vars; std::vector 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(now1 - start); CHECK(elapsed1 < std::chrono::milliseconds(static_cast(hold_time_ms))); // 达到保持时间后 — 可以报警 auto now2 = start + std::chrono::milliseconds(static_cast(hold_time_ms) + 100); auto elapsed2 = std::chrono::duration_cast(now2 - start); CHECK(elapsed2 > std::chrono::milliseconds(static_cast(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(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(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(now - start); // hold_time == 0 → always alarm immediately CHECK(elapsed >= std::chrono::milliseconds(static_cast(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(now1 - start); CHECK(elapsed1 < std::chrono::milliseconds(static_cast(hold_time_ms))); // after hold time expires auto now2 = start + std::chrono::milliseconds(61000); auto elapsed2 = std::chrono::duration_cast(now2 - start); CHECK(elapsed2 > std::chrono::milliseconds(static_cast(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 vars; std::vector 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( 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 vars; std::vector 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( 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 vars; std::vector 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( 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( 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 vars; std::vector 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 vars; std::vector 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(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(t1 - t0).count() / (60.0 * 60000); // Interval 2: 7000ms auto t2 = t1 + std::chrono::milliseconds(7000); running_time += std::chrono::duration_cast(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 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(8u, 3u), 1u); CHECK_EQ(gitbit(8u, 0u), 0u); CHECK_EQ(gitbit(1, 0), 1); CHECK_EQ(gitbit(32768L, 15L), 1L); } // ============================================================================ // 第八部分:Roller3 辅助函数测试 // ============================================================================ // extractTagNumbers — 从表达式中提取 "tagN" 的数字序号 // calculateMedian — 计算数组的中位数 static std::vector extractTagNumbers(const std::string& expr) { std::vector 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 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 data; for (int i = 1; i <= 99; i++) { data.push_back(static_cast(i)); } CHECK_FLOAT_EQ(calculateMedian(data), 50.0, 0.001); } TEST(roller3_calculate_median_unsorted_preserves_order) { // verify the function sorts internally std::vector 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")); }