#include "domain/project_storage.h" #include "services/logic_editor_service.h" #include "services/project_service.h" #include #include namespace { class TestProjectStorage final : public ProjectStorage { public: ProjectSaveResult save(const Project &, const std::string &) override { return {true, ProjectStorageError::None, {}}; } ProjectLoadResult load(const std::string &) override { return {false, {}, ProjectStorageError::FileReadFailed, {}}; } }; void require(bool condition, const std::string &message) { if (!condition) { throw std::runtime_error(message); } } ContactNodeConfig contact(int address) { return {RegisterAddress{RegisterArea::M, address}, ContactMode::NormallyOpen}; } void testStructuredEditingAndNormalization() { TestProjectStorage storage; ProjectService project_service(storage); LogicEditorService service(project_service); const std::string logic_id = service.ensureDefaultLogic().id; const std::string rung_id = service.firstRungId(logic_id); const LogicEditorResult first = service.appendCondition(logic_id, rung_id, contact(0)); const LogicEditorResult second = service.appendCondition(logic_id, rung_id, contact(1)); require(first.succeeded && second.succeeded, "series append must succeed"); const LadderRung *rung = service.findRung(logic_id, rung_id); require(rung->condition->kind == ConditionExpressionKind::Series && rung->condition->children.size() == 2U, "two appended nodes must form a series expression"); const std::string second_expression_id = second.id; const LogicEditorResult parallel = service.addParallelBranch( logic_id, rung_id, {second_expression_id}, contact(2)); require(parallel.succeeded, "parallel insertion must succeed"); rung = service.findRung(logic_id, rung_id); const ConditionExpression *parallel_expression = service.findExpression( logic_id, rung_id, rung->condition->children.at(1).id); require(parallel_expression != nullptr && parallel_expression->kind == ConditionExpressionKind::Parallel, "selected node must become a parallel expression"); const LogicEditorResult nested_series = service.insertConditionAfter( logic_id, rung_id, parallel.id, contact(3)); require(nested_series.succeeded, "a parallel branch must accept a series node"); rung = service.findRung(logic_id, rung_id); require(rung->condition->kind == ConditionExpressionKind::Series, "root must remain a series expression"); const ConditionExpression &nested_parallel_expression = rung->condition->children.at(1); require(nested_parallel_expression.kind == ConditionExpressionKind::Parallel && nested_parallel_expression.children.at(1).kind == ConditionExpressionKind::Series, "editor must express A AND (B OR (C AND D))"); require(service.removeNode(logic_id, nested_series.id).succeeded, "nested series node deletion must succeed"); rung = service.findRung(logic_id, rung_id); require(rung->condition->children.at(1).kind == ConditionExpressionKind::Parallel && rung->condition->children.at(1).children.at(1).kind == ConditionExpressionKind::Node, "single-child series container must collapse after deletion"); require(service.removeNode(logic_id, parallel.id).succeeded, "parallel leaf deletion must succeed"); rung = service.findRung(logic_id, rung_id); require(rung->condition->kind == ConditionExpressionKind::Series && rung->condition->children.size() == 2U, "single-child parallel container must collapse after deletion"); require(service.setOutput( logic_id, rung_id, CoilNodeConfig{RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal}) .succeeded, "output coil must be set"); require(service.findLogic(logic_id)->validate(), "structured editing result must remain a valid draft"); } void testRangeParallelInsertion() { TestProjectStorage storage; ProjectService project_service(storage); LogicEditorService service(project_service); const std::string logic_id = service.ensureDefaultLogic().id; const std::string rung_id = service.firstRungId(logic_id); service.appendCondition(logic_id, rung_id, contact(0)); service.appendCondition(logic_id, rung_id, contact(1)); service.appendCondition(logic_id, rung_id, contact(2)); const LogicEditorResult branch = service.addParallelBranch( logic_id, rung_id, {"contact-2", "contact-3"}, contact(3)); require(branch.succeeded, "a continuous series range must accept a parallel branch"); const ConditionExpression &root = *service.findRung(logic_id, rung_id)->condition; require(root.kind == ConditionExpressionKind::Series && root.children.size() == 2U && root.children.at(1).kind == ConditionExpressionKind::Parallel && root.children.at(1).children.front().kind == ConditionExpressionKind::Series, "range insertion must express A AND ((B AND C) OR D)"); require(root.validate(), "range insertion must preserve normalized topology"); const LogicEditorResult invalid = service.addParallelBranch( logic_id, rung_id, {"contact-1", "contact-3"}, contact(4)); require(!invalid.succeeded && invalid.error == LogicEditorError::InvalidOperation, "a non-contiguous selection must be rejected"); } void testLogicLifecycleAndOrdering() { TestProjectStorage storage; ProjectService project_service(storage); LogicEditorService service(project_service); const std::string first_id = service.ensureDefaultLogic().id; const LogicEditorResult second = service.addLogic("Safety logic"); const LogicEditorResult third = service.addLogic("Alarm logic"); require(second.succeeded && third.succeeded, "multiple control logic modules must be creatable"); require(service.renameLogic(second.id, "Interlock logic").succeeded, "control logic modules must be renamable by stable id"); require(service.renameLogic(third.id, "Interlock logic").error == LogicEditorError::DuplicateName, "control logic names must remain unique"); require(service.moveLogic(third.id, -1).succeeded && project_service.project().controlLogics.at(1).id == third.id, "logic scan order must follow editable vector order"); require(service.setLogicEnabled(second.id, false).succeeded && !service.findLogic(second.id)->enabled, "a control logic module must support explicit disable and enable"); require(service.setLogicEnabled(second.id, true).succeeded && service.findLogic(second.id)->enabled, "a disabled control logic module must be re-enableable"); require(service.removeLogic(third.id).succeeded, "a non-final control logic module must be deletable"); require(service.removeLogic(second.id).succeeded, "logic deletion must preserve the remaining module"); require(service.removeLogic(first_id).error == LogicEditorError::LastLogicRequired, "the project must retain at least one control logic module"); } } // namespace int main() { try { testStructuredEditingAndNormalization(); testRangeParallelInsertion(); testLogicLifecycleAndOrdering(); } catch (const std::exception &error) { std::cerr << "logic editor service tests failed: " << error.what() << '\n'; return 1; } std::cout << "logic editor service tests passed\n"; return 0; }