#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 domain = integrated_platform::domain; namespace { void require(bool condition, const std::string &message) { if (!condition) { throw std::runtime_error(message); } } void testRegisterAddressBoundaries() { require(domain::RegisterAddress{domain::RegisterArea::M, 0}.isValid(), "M0 must be valid"); require(domain::RegisterAddress{domain::RegisterArea::D, 4000}.isValid(), "D4000 must be valid"); require(!(domain::RegisterAddress{domain::RegisterArea::M, -1}.isValid()), "negative register index must be rejected"); require(!(domain::RegisterAddress{domain::RegisterArea::D, 4001}.isValid()), "register index above 4000 must be rejected"); require(!(domain::RegisterAddress{static_cast(99), 0}.isValid()), "unknown register area must be rejected"); } void testRegisterRepositorySeparatesAreas() { domain::VirtualRegisterRepository repository; const domain::RegisterAddress m0{domain::RegisterArea::M, 0}; const domain::RegisterAddress d0{domain::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 == domain::RegisterError::AreaMismatch, "D address must not be read as a bit"); require(repository.readWord(m0).error == domain::RegisterError::AreaMismatch, "M address must not be read as a word"); } domain::Project makeValidProject() { domain::HmiControl start_button; start_button.id = "start-button"; start_button.type = domain::HmiControlType::Button; start_button.text = "Start"; start_button.binding = {domain::RegisterArea::M, 0}; domain::HmiPage page; page.id = "main-page"; page.name = "Main"; page.controls.push_back(start_button); domain::LogicNode contact; contact.id = "start-contact"; contact.config = domain::ContactNodeConfig{ domain::RegisterAddress{domain::RegisterArea::M, 0}, domain::ContactMode::NormallyOpen}; domain::LogicNode coil; coil.id = "run-coil"; coil.config = domain::CoilNodeConfig{ domain::RegisterAddress{domain::RegisterArea::M, 1}, domain::CoilMode::Normal}; domain::ControlLogic logic; logic.id = "start-logic"; logic.name = "Start logic"; logic.nodes = {contact, coil}; logic.connections.push_back({contact.id, coil.id}); domain::Project project; project.metadata = {"sample-project", "Sample project", "1.0"}; project.hmiPages.push_back(page); project.controlLogics.push_back(logic); return project; } void testLogicNodeConfigurationBoundaries() { domain::LogicNode contact; contact.id = "contact"; contact.config = domain::ContactNodeConfig{ domain::RegisterAddress{domain::RegisterArea::M, 0}, domain::ContactMode::NormallyOpen}; require(contact.validate(), "contact node bound to M address must be valid"); contact.config = domain::ContactNodeConfig{ domain::RegisterAddress{domain::RegisterArea::D, 0}, domain::ContactMode::NormallyOpen}; require(!contact.validate(), "contact node bound to D address must be rejected"); domain::LogicNode comparison; comparison.id = "comparison"; comparison.config = domain::CompareNodeConfig{ domain::RegisterAddress{domain::RegisterArea::D, 0}, domain::ComparisonOperator::GreaterThan, static_cast(100)}; require(comparison.validate(), "comparison node bound to D address must be valid"); } void testModelsValidateBindingsAndIdentifiers() { domain::Project project = makeValidProject(); require(project.validate(), "valid project model must pass validation"); project.hmiPages.front().controls.front().binding = domain::RegisterAddress{domain::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"); } void testRuntimeStateBoundaries() { domain::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 == domain::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 == domain::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; }