|
- #include "domain/project_storage.h"
- #include "services/logic_editor_service.h"
- #include "services/project_service.h"
-
- #include <iostream>
- #include <stdexcept>
-
- 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 testStructuredWireEditing()
- {
- 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.addParallelWireBranch(
- logic_id, rung_id, {"contact-2", "contact-3"});
- require(branch.succeeded && branch.id == "wire-1",
- "a continuous range must accept a structured horizontal bypass");
- const LadderRung *rung = service.findRung(logic_id, rung_id);
- require(rung->condition->kind == ConditionExpressionKind::Series
- && rung->condition->children.at(1).kind
- == ConditionExpressionKind::Parallel,
- "a vertical connection must produce a parallel expression");
- const ConditionExpression &wire =
- rung->condition->children.at(1).children.at(1);
- require(wire.kind == ConditionExpressionKind::Wire
- && wire.wire->columnSpan == 2,
- "the bypass wire span must match the selected two-column range");
-
- const LogicEditorResult replacement = service.replaceWireWithCondition(
- logic_id, rung_id, branch.id, contact(3));
- require(replacement.succeeded && replacement.id == "contact-4",
- "a selected wire must be replaceable by a configured node type");
- const LogicEditorResult extension = service.insertWireAfter(
- logic_id, rung_id, replacement.id);
- require(extension.succeeded && extension.id == "wire-1",
- "a wire id must become reusable after its expression is replaced");
-
- rung = service.findRung(logic_id, rung_id);
- const ConditionExpression *extended_branch = service.findExpression(
- logic_id, rung_id, rung->condition->children.at(1).children.at(1).id);
- require(extended_branch != nullptr
- && extended_branch->kind == ConditionExpressionKind::Series
- && extended_branch->children.at(1).kind
- == ConditionExpressionKind::Wire,
- "inserting a horizontal wire after a branch node must preserve structure");
- const std::string extended_branch_id = extended_branch->id;
- require(!service.addParallelWireBranch(
- logic_id, rung_id, {"contact-1", "contact-3"}).succeeded,
- "non-contiguous wire connection targets must be rejected");
-
- require(service.removeExpressions(
- logic_id, rung_id, {extended_branch_id}).succeeded,
- "deleting a selected vertical connection branch must succeed atomically");
- rung = service.findRung(logic_id, rung_id);
- require(rung->condition->kind == ConditionExpressionKind::Series
- && rung->condition->children.size() == 3U,
- "removing a bypass branch must normalize back to the original series");
- require(service.undo().succeeded
- && service.findExpression(logic_id, rung_id, extended_branch_id) != nullptr,
- "wire branch deletion must participate in ladder undo history");
- }
-
- void testConditionColumnLimit()
- {
- 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);
-
- for (int column = 0; column < ProjectLimits::kMaximumConditionColumns; ++column)
- {
- require(service.appendCondition(logic_id, rung_id, contact(column)).succeeded,
- "the first ten condition columns must be editable");
- }
- const LogicEditorResult overflow = service.appendCondition(
- logic_id, rung_id, contact(ProjectLimits::kMaximumConditionColumns));
- require(!overflow.succeeded
- && overflow.error == LogicEditorError::InvalidOperation,
- "the eleventh condition column must be rejected by the editor service");
- const LadderRung *rung = service.findRung(logic_id, rung_id);
- require(rung != nullptr && rung->condition.has_value()
- && rung->condition->kind == ConditionExpressionKind::Series
- && rung->condition->children.size()
- == static_cast<std::size_t>(
- ProjectLimits::kMaximumConditionColumns),
- "a rejected eleventh column must leave the ten-column network unchanged");
- }
-
- void testUnconditionalOutputEditing()
- {
- 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);
-
- require(service.setOutput(
- logic_id,
- rung_id,
- CoilNodeConfig{
- RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal},
- true)
- .succeeded,
- "the editor must allow a coil before any condition is added");
- const LadderRung *rung = service.findRung(logic_id, rung_id);
- require(rung != nullptr && !rung->condition.has_value()
- && rung->output.has_value() && rung->validateForRunning(),
- "an editor-created output-only network must be runnable as unconditional");
- }
-
- 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");
- }
-
- void testEdgeTimerNodesAndRungComments()
- {
- 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 rising = service.appendCondition(
- logic_id,
- rung_id,
- EdgeContactNodeConfig{
- RegisterAddress{RegisterArea::M, 3}, EdgeMode::Rising});
- require(rising.succeeded && rising.id == "edge-1",
- "the editor must create rising edge nodes with a stable prefix");
- const LogicEditorResult timer = service.addParallelBranch(
- logic_id,
- rung_id,
- {rising.id},
- TimerContactNodeConfig{TimerAddress{2}, ContactMode::NormallyOpen});
- require(timer.succeeded && timer.id == "timer-contact-1",
- "the editor must insert T contacts as conditions");
- const LogicEditorResult ton = service.setOutput(
- logic_id, rung_id, TonNodeConfig{TimerAddress{2}, 500});
- require(ton.succeeded && ton.id == "ton-1",
- "the editor must create TON outputs with a stable prefix");
-
- require(service.updateNodeConfig(
- logic_id,
- rising.id,
- EdgeContactNodeConfig{
- RegisterAddress{RegisterArea::M, 4}, EdgeMode::Falling})
- .succeeded,
- "the editor must apply edge mode and M address properties");
- require(service.updateNodeConfig(
- logic_id,
- timer.id,
- TimerContactNodeConfig{TimerAddress{4}, ContactMode::NormallyClosed})
- .succeeded,
- "the editor must apply T contact mode and address properties");
- require(service.updateNodeConfig(
- logic_id,
- ton.id,
- TonNodeConfig{TimerAddress{4}, 750})
- .succeeded,
- "the editor must apply TON preset properties");
- require(service.updateRungComment(logic_id, rung_id, "延时启动网络").succeeded,
- "the editor must update a network comment by stable rung id");
-
- const LadderRung *rung = service.findRung(logic_id, rung_id);
- require(rung != nullptr && rung->comment == "延时启动网络"
- && std::get<EdgeContactNodeConfig>(
- rung->condition->children.front().node->config).mode
- == EdgeMode::Falling
- && std::get<TimerContactNodeConfig>(
- rung->condition->children.at(1).node->config).address
- == TimerAddress{4}
- && std::get<TonNodeConfig>(rung->output->config).presetMs == 750,
- "edge, T contact, TON and rung comment updates must remain in the model");
- }
-
- void testHistoryAndAtomicBatchDelete()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage);
- LogicEditorService service(project_service);
- const std::string logic_id = service.ensureDefaultLogic().id;
- const std::string first_rung_id = service.firstRungId(logic_id);
- const LogicEditorResult first = service.appendCondition(
- logic_id, first_rung_id, contact(0));
- const LogicEditorResult second = service.appendCondition(
- logic_id, first_rung_id, contact(1));
- const LogicEditorResult second_rung = service.addRung(logic_id);
- const LogicEditorResult third = service.appendCondition(
- logic_id, second_rung.id, contact(2));
- require(first.succeeded && second.succeeded && second_rung.succeeded
- && third.succeeded,
- "nodes for history testing must be created");
-
- service.clearHistory();
- require(!service.removeNodes(logic_id, {first.id, "missing-node"}).succeeded,
- "batch node deletion must validate every id before changing the logic");
- require(service.findNode(logic_id, first.id) != nullptr
- && service.findNode(logic_id, third.id) != nullptr
- && !service.canUndo(),
- "failed batch node deletion must be atomic and leave history unchanged");
-
- require(service.removeNodes(logic_id, {first.id, third.id}).succeeded,
- "valid nodes across multiple rungs must be deleted together");
- require(service.findNode(logic_id, first.id) == nullptr
- && service.findNode(logic_id, third.id) == nullptr,
- "all selected nodes must be removed by one batch operation");
- require(service.undo().succeeded
- && service.findNode(logic_id, first.id) != nullptr
- && service.findNode(logic_id, third.id) != nullptr,
- "logic undo must restore a cross-rung batch deletion");
- require(service.redo().succeeded
- && service.findNode(logic_id, first.id) == nullptr
- && service.findNode(logic_id, third.id) == nullptr,
- "logic redo must reapply a cross-rung batch deletion");
-
- service.clearHistory();
- const ControlLogic *logic = service.findLogic(logic_id);
- require(logic != nullptr && service.setLogicEnabled(logic_id, logic->enabled).succeeded
- && !service.canUndo(),
- "setting an unchanged logic state must not consume history");
-
- service.clearHistory();
- for (int index = 1; index <= 101; ++index)
- {
- require(service.updateRungComment(
- logic_id, first_rung_id, "comment-" + std::to_string(index))
- .succeeded,
- "repeated valid rung edits must succeed");
- }
- int undo_count = 0;
- while (service.undo().succeeded)
- {
- ++undo_count;
- }
- require(undo_count == 100,
- "logic history must retain exactly the configured 100 most recent steps");
-
- require(service.redo().succeeded,
- "logic redo must be available after an undo");
- require(service.appendCondition(logic_id, first_rung_id, contact(5)).succeeded,
- "a new logic edit must succeed after undo");
- require(!service.canRedo(),
- "a new logic edit must clear the redo history");
- (void)second;
- }
-
- } // namespace
-
- int main()
- {
- try
- {
- testStructuredEditingAndNormalization();
- testRangeParallelInsertion();
- testStructuredWireEditing();
- testConditionColumnLimit();
- testUnconditionalOutputEditing();
- testLogicLifecycleAndOrdering();
- testEdgeTimerNodesAndRungComments();
- testHistoryAndAtomicBatchDelete();
- }
- 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;
- }
|