#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 #include #include #include #include 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(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(-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"; logic.nodes = {contact, coil}; logic.connections.push_back({contact.id, coil.id}); 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(100)}; require(comparison.validate(), "comparison node bound to D address must be valid"); } 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"); } 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(); 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; }