|
- #include "domain/control_logic_model.h"
- #include "domain/hmi_model.h"
- #include "domain/project_model.h"
- #include "domain/register_address.h"
- #include "domain/register_repository.h"
- #include "domain/runtime_state.h"
-
- #include <cstdint>
- #include <exception>
- #include <iostream>
- #include <stdexcept>
- #include <string>
-
- namespace {
-
- void require(bool condition, const std::string &message)
- {
- if (!condition)
- {
- throw std::runtime_error(message);
- }
- }
-
- void testRegisterAddressBoundaries()
- {
- // 覆盖 M/D 地址允许范围及未知枚举值的拒绝路径
- require(RegisterAddress{RegisterArea::M, 0}.isValid(),
- "M0 must be valid");
- require(RegisterAddress{RegisterArea::D, 4000}.isValid(),
- "D4000 must be valid");
- require(!(RegisterAddress{RegisterArea::M, -1}.isValid()),
- "negative register index must be rejected");
- require(!(RegisterAddress{RegisterArea::D, 4001}.isValid()),
- "register index above 4000 must be rejected");
- require(!(RegisterAddress{static_cast<RegisterArea>(99), 0}.isValid()),
- "unknown register area must be rejected");
- }
-
- void testRegisterRepositorySeparatesAreas()
- {
- // 验证离线仓库不会把 M 位和 D 字交叉解释
- VirtualRegisterRepository repository;
- const RegisterAddress m0{RegisterArea::M, 0};
- const RegisterAddress d0{RegisterArea::D, 0};
-
- require(repository.writeBit(m0, true).succeeded, "M bit write must succeed");
- require(repository.readBit(m0).value, "M bit read must return written value");
- require(repository.writeWord(d0, static_cast<std::int16_t>(-123)).succeeded,
- "D word write must succeed");
- require(repository.readWord(d0).value == -123, "D word read must return written value");
- require(repository.readBit(d0).error == RegisterError::AreaMismatch,
- "D address must not be read as a bit");
- require(repository.readWord(m0).error == RegisterError::AreaMismatch,
- "M address must not be read as a word");
- }
-
- Project makeValidProject()
- {
- // 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线
- HmiControl start_button;
- start_button.id = "start-button";
- start_button.type = HmiControlType::Button;
- start_button.text = "Start";
- start_button.binding = {RegisterArea::M, 0};
-
- HmiPage page;
- page.id = "main-page";
- page.name = "Main";
- page.controls.push_back(start_button);
-
- LogicNode contact;
- contact.id = "start-contact";
- contact.config = ContactNodeConfig{
- RegisterAddress{RegisterArea::M, 0},
- ContactMode::NormallyOpen};
-
- LogicNode coil;
- coil.id = "run-coil";
- coil.config = CoilNodeConfig{
- RegisterAddress{RegisterArea::M, 1},
- CoilMode::Normal};
-
- ControlLogic logic;
- logic.id = "start-logic";
- logic.name = "Start logic";
- LadderStage stage;
- stage.id = "stage-1";
- stage.branches.push_back(contact);
- LadderRung rung;
- rung.id = "rung-1";
- rung.name = "Network 1";
- rung.stages.push_back(stage);
- rung.output = coil;
- logic.rungs.push_back(rung);
-
- Project project;
- project.metadata = {"sample-project", "Sample project", "1.0"};
- project.hmiPages.push_back(page);
- project.controlLogics.push_back(logic);
- return project;
- }
-
- void testLogicNodeConfigurationBoundaries()
- {
- // 触点只能绑定 M 区,数值比较只能绑定 D 区
- LogicNode contact;
- contact.id = "contact";
- contact.config = ContactNodeConfig{
- RegisterAddress{RegisterArea::M, 0},
- ContactMode::NormallyOpen};
- require(contact.validate(), "contact node bound to M address must be valid");
-
- contact.config = ContactNodeConfig{
- RegisterAddress{RegisterArea::D, 0},
- ContactMode::NormallyOpen};
- require(!contact.validate(), "contact node bound to D address must be rejected");
-
- LogicNode comparison;
- comparison.id = "comparison";
- comparison.config = CompareNodeConfig{
- RegisterAddress{RegisterArea::D, 0},
- ComparisonOperator::GreaterThan,
- static_cast<std::int16_t>(100)};
- require(comparison.validate(), "comparison node bound to D address must be valid");
- }
-
- void testLadderLogicBoundaries()
- {
- LogicNode stop;
- stop.id = "stop";
- stop.config = ContactNodeConfig{
- RegisterAddress{RegisterArea::M, 1},
- ContactMode::NormallyClosed};
-
- LogicNode start;
- start.id = "start";
- start.config = ContactNodeConfig{
- RegisterAddress{RegisterArea::M, 0},
- ContactMode::NormallyOpen};
-
- LogicNode run_contact;
- run_contact.id = "run-contact";
- run_contact.config = ContactNodeConfig{
- RegisterAddress{RegisterArea::M, 1},
- ContactMode::NormallyOpen};
-
- LogicNode coil;
- coil.id = "run-coil";
- coil.config = CoilNodeConfig{
- RegisterAddress{RegisterArea::M, 1},
- CoilMode::Normal};
-
- ControlLogic logic;
- logic.id = "hold-logic";
- logic.name = "Hold logic";
- LadderRung rung;
- rung.id = "rung-1";
- rung.name = "Self hold";
- rung.stages.push_back({"stage-stop", {stop}});
- rung.stages.push_back({"stage-start", {start, run_contact}});
- rung.output = coil;
- logic.rungs.push_back(rung);
- require(logic.validate(), "stop AND (start OR run) self-hold ladder must be valid");
-
- logic.rungs.front().stages.front().branches.push_back(coil);
- require(!logic.validate(), "a ladder condition stage must reject coils");
-
- logic.rungs.front().stages.front().branches.pop_back();
- logic.rungs.front().output = start;
- require(!logic.validate(), "a ladder output must be a coil");
-
- logic.rungs.front().output.reset();
- require(logic.validate(), "incomplete ladder may remain in an editable draft");
- require(!logic.validateForRunning(),
- "conditions without an output must block runtime validation");
-
- LadderRung empty_rung{"rung-empty", "Empty network", {}, std::nullopt};
- require(empty_rung.validate(), "an empty editing network must be valid");
-
- empty_rung.output = coil;
- require(empty_rung.validate(), "output-only network may remain in an editable draft");
- require(!empty_rung.validateForRunning(),
- "an output without conditions must block runtime validation");
-
- logic.rungs.front().output = coil;
- logic.rungs.front().stages.at(1).branches.at(1).id = start.id;
- require(!logic.validate(), "logic node ids must be unique");
- }
-
- void testModelsValidateBindingsAndIdentifiers()
- {
- // 聚合验证必须拒绝错误绑定、重复标识和越界控件
- Project project = makeValidProject();
- require(project.validate(), "valid project model must pass validation");
-
- project.hmiPages.front().controls.front().binding =
- RegisterAddress{RegisterArea::D, 0};
- require(!project.validate(), "button bound to D area must be rejected");
-
- project = makeValidProject();
- project.hmiPages.push_back(project.hmiPages.front());
- require(!project.validate(), "duplicate HMI page id must be rejected");
-
- project = makeValidProject();
- project.hmiPages.front().controls.front().bounds.x = -1;
- require(!project.validate(), "controls outside the page must be rejected");
-
- project = makeValidProject();
- project.hmiPages.front().controls.front().bounds.width = 801;
- require(!project.validate(), "controls wider than the page must be rejected");
-
- project = makeValidProject();
- project.hmiPages.front().controls.front().properties.emplace("", "value");
- require(!project.validate(), "empty HMI property names must be rejected");
-
- project = makeValidProject();
- project.hmiPages.front().controls.front().binding.reset();
- require(project.validate(), "unbound HMI control must be accepted in a draft");
- require(!project.validateForRunning(),
- "unbound HMI control must block runtime validation");
-
- project = makeValidProject();
- project.controlLogics.front().rungs.front().output->configured = false;
- require(project.validate(), "unconfigured ladder node must be accepted in a draft");
- require(!project.validateForRunning(),
- "unconfigured ladder node must block runtime validation");
- }
-
- void testRuntimeStateBoundaries()
- {
- // 运行模式测试覆盖离线和真机的互斥及 PLC 首读前置条件
- RuntimeState state;
- require(state.policy().allowsProjectEditing, "editing mode must allow project editing");
- require(state.enterOfflineRunning().succeeded, "editing may enter offline running");
- require(state.policy().usesVirtualRegisters, "offline mode must use virtual registers");
- require(state.policy().runsLogicExecutor, "offline mode must run logic executor");
- require(state.enterOnlineRunning(true).error
- == ModeTransitionError::MustReturnToEditing,
- "offline mode must not directly enter online mode");
-
- require(state.enterEditing().succeeded, "offline mode may return to editing");
- require(state.enterOnlineRunning(false).error
- == ModeTransitionError::InitialPlcReadRequired,
- "online mode must require an initial PLC read");
- require(state.enterOnlineRunning(true).succeeded,
- "editing may enter online mode after initial PLC read");
- require(!state.policy().runsLogicExecutor,
- "online mode must keep the software logic executor stopped");
- require(state.policy().usesPlcRegisters, "online mode must use PLC registers");
- }
-
- } // namespace
-
- int main()
- {
- try
- {
- // 每个测试函数独立覆盖一个领域边界,首个异常即终止测试进程
- testRegisterAddressBoundaries();
- testRegisterRepositorySeparatesAreas();
- testLogicNodeConfigurationBoundaries();
- testLadderLogicBoundaries();
- testModelsValidateBindingsAndIdentifiers();
- testRuntimeStateBoundaries();
- }
- catch (const std::exception &error)
- {
- std::cerr << "domain tests failed: " << error.what() << '\n';
- return 1;
- }
-
- std::cout << "domain tests passed\n";
- return 0;
- }
|