综合平台编程器项目的远程存储
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

836 řádky
39 KiB

  1. #include "domain/project_storage.h"
  2. #include "domain/project_limits.h"
  3. #include "services/logic_editor_service.h"
  4. #include "services/project_service.h"
  5. #include <algorithm>
  6. #include <iostream>
  7. #include <stdexcept>
  8. namespace {
  9. class TestProjectStorage final : public ProjectStorage
  10. {
  11. public:
  12. ProjectSaveResult save(const Project &, const std::string &) override
  13. {
  14. return {true, ProjectStorageError::None, {}};
  15. }
  16. ProjectLoadResult load(const std::string &) override
  17. {
  18. return {false, {}, ProjectStorageError::FileReadFailed, {}};
  19. }
  20. };
  21. void require(bool condition, const std::string &message)
  22. {
  23. if (!condition)
  24. {
  25. throw std::runtime_error(message);
  26. }
  27. }
  28. ContactNodeConfig contact(int address)
  29. {
  30. return {RegisterAddress{RegisterArea::M, address}, ContactMode::NormallyOpen};
  31. }
  32. int conditionColumns(const ConditionExpression &expression)
  33. {
  34. if (expression.kind == ConditionExpressionKind::Node)
  35. {
  36. return 1;
  37. }
  38. if (expression.kind == ConditionExpressionKind::Wire)
  39. {
  40. return expression.wire->columnSpan;
  41. }
  42. int columns = expression.kind == ConditionExpressionKind::Series ? 0 : 1;
  43. for (const ConditionExpression &child : expression.children)
  44. {
  45. const int child_columns = conditionColumns(child);
  46. columns = expression.kind == ConditionExpressionKind::Series
  47. ? columns + child_columns : std::max(columns, child_columns);
  48. }
  49. return columns;
  50. }
  51. int wireColumns(const ConditionExpression &expression)
  52. {
  53. if (expression.kind == ConditionExpressionKind::Node)
  54. {
  55. return 0;
  56. }
  57. if (expression.kind == ConditionExpressionKind::Wire)
  58. {
  59. return expression.wire->columnSpan;
  60. }
  61. int columns = 0;
  62. for (const ConditionExpression &child : expression.children)
  63. {
  64. columns += wireColumns(child);
  65. }
  66. return columns;
  67. }
  68. void testStructuredEditingAndNormalization()
  69. {
  70. TestProjectStorage storage;
  71. ProjectService project_service(storage);
  72. LogicEditorService service(project_service);
  73. const std::string logic_id = service.ensureDefaultLogic().id;
  74. const std::string rung_id = service.firstRungId(logic_id);
  75. const LogicEditorResult first = service.appendCondition(logic_id, rung_id, contact(0));
  76. const LogicEditorResult second = service.appendCondition(logic_id, rung_id, contact(1));
  77. require(first.succeeded && second.succeeded, "series append must succeed");
  78. const LadderRung *rung = service.findRung(logic_id, rung_id);
  79. require(rung->condition->kind == ConditionExpressionKind::Series
  80. && rung->condition->children.size() == 2U,
  81. "two appended nodes must form a series expression");
  82. const std::string second_expression_id = second.id;
  83. const LogicEditorResult parallel = service.addParallelBranch(
  84. logic_id, rung_id, {second_expression_id}, contact(2));
  85. require(parallel.succeeded, "parallel insertion must succeed");
  86. rung = service.findRung(logic_id, rung_id);
  87. const ConditionExpression *parallel_expression = service.findExpression(
  88. logic_id, rung_id, rung->condition->children.at(1).id);
  89. require(parallel_expression != nullptr
  90. && parallel_expression->kind == ConditionExpressionKind::Parallel,
  91. "selected node must become a parallel expression");
  92. const LogicEditorResult nested_series = service.insertConditionAfter(
  93. logic_id,
  94. rung_id,
  95. parallel.id,
  96. contact(3));
  97. require(nested_series.succeeded, "a parallel branch must accept a series node");
  98. rung = service.findRung(logic_id, rung_id);
  99. require(rung->condition->kind == ConditionExpressionKind::Series,
  100. "root must remain a series expression");
  101. const ConditionExpression &nested_parallel_expression = rung->condition->children.at(1);
  102. require(nested_parallel_expression.kind == ConditionExpressionKind::Parallel
  103. && nested_parallel_expression.children.at(1).kind
  104. == ConditionExpressionKind::Series,
  105. "editor must express A AND (B OR (C AND D))");
  106. require(service.removeNode(logic_id, nested_series.id).succeeded,
  107. "nested series node deletion must succeed");
  108. rung = service.findRung(logic_id, rung_id);
  109. require(rung->condition->children.at(1).kind == ConditionExpressionKind::Parallel
  110. && rung->condition->children.at(1).children.at(1).kind
  111. == ConditionExpressionKind::Node,
  112. "single-child series container must collapse after deletion");
  113. require(service.removeNode(logic_id, parallel.id).succeeded,
  114. "parallel leaf deletion must succeed");
  115. rung = service.findRung(logic_id, rung_id);
  116. require(rung->condition->kind == ConditionExpressionKind::Series
  117. && rung->condition->children.size() == 2U,
  118. "single-child parallel container must collapse after deletion");
  119. require(service.setOutput(
  120. logic_id,
  121. rung_id,
  122. CoilNodeConfig{RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal})
  123. .succeeded,
  124. "output coil must be set");
  125. require(service.findLogic(logic_id)->validate(),
  126. "structured editing result must remain a valid draft");
  127. }
  128. void testRangeParallelInsertion()
  129. {
  130. TestProjectStorage storage;
  131. ProjectService project_service(storage);
  132. LogicEditorService service(project_service);
  133. const std::string logic_id = service.ensureDefaultLogic().id;
  134. const std::string rung_id = service.firstRungId(logic_id);
  135. service.appendCondition(logic_id, rung_id, contact(0));
  136. service.appendCondition(logic_id, rung_id, contact(1));
  137. service.appendCondition(logic_id, rung_id, contact(2));
  138. const LogicEditorResult branch = service.addParallelBranch(
  139. logic_id, rung_id, {"contact-2", "contact-3"}, contact(3));
  140. require(branch.succeeded, "a continuous series range must accept a parallel branch");
  141. const ConditionExpression &root = *service.findRung(logic_id, rung_id)->condition;
  142. require(root.kind == ConditionExpressionKind::Series
  143. && root.children.size() == 2U
  144. && root.children.at(1).kind == ConditionExpressionKind::Parallel
  145. && root.children.at(1).children.front().kind
  146. == ConditionExpressionKind::Series,
  147. "range insertion must express A AND ((B AND C) OR D)");
  148. require(root.validate(), "range insertion must preserve normalized topology");
  149. const LogicEditorResult invalid = service.addParallelBranch(
  150. logic_id, rung_id, {"contact-1", "contact-3"}, contact(4));
  151. require(!invalid.succeeded
  152. && invalid.error == LogicEditorError::InvalidOperation,
  153. "a non-contiguous selection must be rejected");
  154. }
  155. void testStructuredWireEditing()
  156. {
  157. TestProjectStorage storage;
  158. ProjectService project_service(storage);
  159. LogicEditorService service(project_service);
  160. const std::string logic_id = service.ensureDefaultLogic().id;
  161. const std::string rung_id = service.firstRungId(logic_id);
  162. service.appendCondition(logic_id, rung_id, contact(0));
  163. service.appendCondition(logic_id, rung_id, contact(1));
  164. service.appendCondition(logic_id, rung_id, contact(2));
  165. const LogicEditorResult branch = service.addParallelWireBranch(
  166. logic_id, rung_id, {"contact-2", "contact-3"});
  167. require(branch.succeeded && branch.id == "wire-1",
  168. "a continuous range must accept a structured horizontal bypass");
  169. const LadderRung *rung = service.findRung(logic_id, rung_id);
  170. require(rung->condition->kind == ConditionExpressionKind::Series
  171. && rung->condition->children.at(1).kind
  172. == ConditionExpressionKind::Parallel,
  173. "a vertical connection must produce a parallel expression");
  174. const ConditionExpression &wire =
  175. rung->condition->children.at(1).children.at(1);
  176. require(wire.kind == ConditionExpressionKind::Wire
  177. && wire.wire->columnSpan == 2,
  178. "the bypass wire span must match the selected two-column range");
  179. const LogicEditorResult replacement = service.replaceWireWithCondition(
  180. logic_id, rung_id, branch.id, contact(3));
  181. require(replacement.succeeded && replacement.id == "contact-4",
  182. "a selected wire must be replaceable by a configured node type");
  183. const LogicEditorResult extension = service.insertWireAfter(
  184. logic_id, rung_id, replacement.id);
  185. require(extension.succeeded && extension.id == "wire-1",
  186. "a wire id must become reusable after its expression is replaced");
  187. rung = service.findRung(logic_id, rung_id);
  188. const ConditionExpression *extended_branch = service.findExpression(
  189. logic_id, rung_id, rung->condition->children.at(1).children.at(1).id);
  190. require(extended_branch != nullptr
  191. && extended_branch->kind == ConditionExpressionKind::Series
  192. && extended_branch->children.at(1).kind
  193. == ConditionExpressionKind::Wire,
  194. "inserting a horizontal wire after a branch node must preserve structure");
  195. const std::string extended_branch_id = extended_branch->id;
  196. require(!service.addParallelWireBranch(
  197. logic_id, rung_id, {"contact-1", "contact-3"}).succeeded,
  198. "non-contiguous wire connection targets must be rejected");
  199. require(service.removeExpressions(
  200. logic_id, rung_id, {extended_branch_id}).succeeded,
  201. "deleting a selected vertical connection branch must succeed atomically");
  202. rung = service.findRung(logic_id, rung_id);
  203. require(rung->condition->kind == ConditionExpressionKind::Series
  204. && rung->condition->children.size() == 3U,
  205. "removing a bypass branch must normalize back to the original series");
  206. require(service.undo().succeeded
  207. && service.findExpression(logic_id, rung_id, extended_branch_id) != nullptr,
  208. "wire branch deletion must participate in ladder undo history");
  209. }
  210. void testBatchDeleteAllNodesInParallelBranch()
  211. {
  212. TestProjectStorage storage;
  213. ProjectService project_service(storage);
  214. LogicEditorService service(project_service);
  215. const std::string logic_id = service.ensureDefaultLogic().id;
  216. const std::string rung_id = service.firstRungId(logic_id);
  217. const LogicEditorResult first = service.appendCondition(
  218. logic_id, rung_id, contact(0));
  219. const LogicEditorResult second = service.appendCondition(
  220. logic_id, rung_id, contact(1));
  221. const LogicEditorResult third = service.appendCondition(
  222. logic_id, rung_id, contact(2));
  223. const LogicEditorResult fourth = service.appendCondition(
  224. logic_id, rung_id, contact(3));
  225. require(first.succeeded && second.succeeded && third.succeeded
  226. && fourth.succeeded,
  227. "parallel batch deletion setup contacts must be created");
  228. const LogicEditorResult branch = service.addParallelBranch(
  229. logic_id, rung_id,
  230. {second.id, third.id, fourth.id},
  231. contact(10));
  232. require(branch.succeeded,
  233. "parallel batch deletion setup branch must be created");
  234. require(service.removeNodes(
  235. logic_id, {second.id, third.id, fourth.id})
  236. .succeeded,
  237. "deleting every node in a parallel branch as one batch must succeed");
  238. const LadderRung *rung = service.findRung(logic_id, rung_id);
  239. require(rung != nullptr && rung->condition.has_value()
  240. && rung->validate(),
  241. "batch deletion must leave a valid normalized ladder expression");
  242. require(rung->condition->kind == ConditionExpressionKind::Series
  243. && rung->condition->children.size() == 2U
  244. && rung->condition->children.at(0).kind
  245. == ConditionExpressionKind::Node
  246. && rung->condition->children.at(1).kind
  247. == ConditionExpressionKind::Node,
  248. "an empty parallel branch must collapse into the remaining branch");
  249. require(service.findNode(logic_id, second.id) == nullptr
  250. && service.findNode(logic_id, third.id) == nullptr
  251. && service.findNode(logic_id, fourth.id) == nullptr,
  252. "all selected parallel branch nodes must be removed");
  253. require(service.undo().succeeded,
  254. "parallel batch deletion must be undoable");
  255. require(service.findNode(logic_id, second.id) != nullptr
  256. && service.findNode(logic_id, third.id) != nullptr
  257. && service.findNode(logic_id, fourth.id) != nullptr,
  258. "undo must restore every deleted parallel branch node");
  259. }
  260. void testConditionColumnLimit()
  261. {
  262. TestProjectStorage storage;
  263. ProjectService project_service(storage);
  264. LogicEditorService service(project_service);
  265. const std::string logic_id = service.ensureDefaultLogic().id;
  266. const std::string rung_id = service.firstRungId(logic_id);
  267. for (int column = 0; column < ProjectLimits::kMaximumConditionColumns; ++column)
  268. {
  269. require(service.appendCondition(logic_id, rung_id, contact(column)).succeeded,
  270. "the first ten condition columns must be editable");
  271. }
  272. const LogicEditorResult overflow = service.appendCondition(
  273. logic_id, rung_id, contact(ProjectLimits::kMaximumConditionColumns));
  274. require(!overflow.succeeded
  275. && overflow.error == LogicEditorError::InvalidOperation,
  276. "the eleventh condition column must be rejected by the editor service");
  277. const LadderRung *rung = service.findRung(logic_id, rung_id);
  278. require(rung != nullptr && rung->condition.has_value()
  279. && rung->condition->kind == ConditionExpressionKind::Series
  280. && rung->condition->children.size()
  281. == static_cast<std::size_t>(
  282. ProjectLimits::kMaximumConditionColumns),
  283. "a rejected eleventh column must leave the ten-column network unchanged");
  284. }
  285. void testUnconditionalOutputEditing()
  286. {
  287. TestProjectStorage storage;
  288. ProjectService project_service(storage);
  289. LogicEditorService service(project_service);
  290. const std::string logic_id = service.ensureDefaultLogic().id;
  291. const std::string rung_id = service.firstRungId(logic_id);
  292. require(service.setOutput(
  293. logic_id,
  294. rung_id,
  295. CoilNodeConfig{
  296. RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal},
  297. true)
  298. .succeeded,
  299. "the editor must allow a coil before any condition is added");
  300. const LadderRung *rung = service.findRung(logic_id, rung_id);
  301. require(rung != nullptr && !rung->condition.has_value()
  302. && rung->output.has_value() && rung->validateForRunning(),
  303. "an editor-created output-only network must be runnable as unconditional");
  304. }
  305. void testColumnTargetedConditionInsertion()
  306. {
  307. TestProjectStorage storage;
  308. ProjectService project_service(storage);
  309. LogicEditorService service(project_service);
  310. const std::string logic_id = service.ensureDefaultLogic().id;
  311. const std::string first_rung_id = service.firstRungId(logic_id);
  312. require(service.insertConditionAtColumn(
  313. logic_id, first_rung_id, 4, contact(4)).succeeded,
  314. "an empty network must accept a condition at the selected fifth column");
  315. const LadderRung *rung = service.findRung(logic_id, first_rung_id);
  316. require(rung != nullptr && rung->condition.has_value()
  317. && rung->condition->kind == ConditionExpressionKind::Series
  318. && rung->condition->children.size() == 2U
  319. && rung->condition->children.front().kind
  320. == ConditionExpressionKind::Wire
  321. && rung->condition->children.front().wire->columnSpan == 4
  322. && rung->condition->children.back().kind
  323. == ConditionExpressionKind::Node
  324. && rung->validate()
  325. && conditionColumns(*rung->condition) == 5,
  326. "column insertion must preserve the requested horizontal position");
  327. require(service.insertConditionAtColumn(
  328. logic_id, first_rung_id, 2, contact(2)).error
  329. == LogicEditorError::InvalidOperation,
  330. "inserting into an already occupied column must be rejected atomically");
  331. require(service.insertConditionAtColumn(
  332. logic_id, first_rung_id, 10, contact(10)).error
  333. == LogicEditorError::InvalidOperation,
  334. "the eleventh condition column must be rejected");
  335. require(service.insertConditionAtColumn(
  336. logic_id, first_rung_id, -1, contact(10)).error
  337. == LogicEditorError::InvalidOperation,
  338. "a negative grid column must be rejected");
  339. require(service.insertConditionAtColumn(
  340. logic_id,
  341. first_rung_id,
  342. 5,
  343. CoilNodeConfig{
  344. RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal})
  345. .error == LogicEditorError::InvalidNode,
  346. "a condition grid slot must reject output instructions");
  347. const LogicEditorResult second_rung = service.addRung(logic_id);
  348. require(second_rung.succeeded,
  349. "column insertion test must create a second empty network");
  350. require(service.setOutput(
  351. logic_id,
  352. second_rung.id,
  353. CoilNodeConfig{RegisterAddress{RegisterArea::M, 20}, CoilMode::Normal},
  354. true)
  355. .succeeded,
  356. "an output-only network must be configurable before adding a condition");
  357. require(service.insertConditionAtColumn(
  358. logic_id, second_rung.id, 0, contact(0)).succeeded,
  359. "a selected first grid slot must work on an output-only network");
  360. const LadderRung *output_rung = service.findRung(logic_id, second_rung.id);
  361. require(output_rung != nullptr && output_rung->condition.has_value()
  362. && output_rung->condition->kind == ConditionExpressionKind::Node
  363. && output_rung->output.has_value()
  364. && output_rung->validate(),
  365. "condition insertion must keep the independent output slot intact");
  366. const LogicEditorResult last_column_rung = service.addRung(logic_id);
  367. require(last_column_rung.succeeded
  368. && service.insertConditionAtColumn(
  369. logic_id, last_column_rung.id, 9, contact(9)).succeeded,
  370. "the tenth condition column must remain a valid insertion target");
  371. const LadderRung *full_width = service.findRung(
  372. logic_id, last_column_rung.id);
  373. require(full_width != nullptr && full_width->condition.has_value()
  374. && full_width->condition->kind == ConditionExpressionKind::Series
  375. && full_width->condition->children.front().wire->columnSpan == 9
  376. && conditionColumns(*full_width->condition) == 10,
  377. "last-column insertion must fill exactly the ten-column condition area");
  378. require(!service.appendCondition(
  379. logic_id, last_column_rung.id, contact(10)).succeeded
  380. && conditionColumns(*service.findRung(
  381. logic_id, last_column_rung.id)->condition) == 10,
  382. "a full-width grid must reject another condition without partial changes");
  383. }
  384. void testWireColumnReplacement()
  385. {
  386. TestProjectStorage storage;
  387. ProjectService project_service(storage);
  388. LogicEditorService service(project_service);
  389. const std::string logic_id = service.ensureDefaultLogic().id;
  390. const std::string rung_id = service.firstRungId(logic_id);
  391. const LogicEditorResult wire = service.appendWire(logic_id, rung_id, 4);
  392. require(wire.succeeded,
  393. "wire-column replacement test must create a four-column wire");
  394. require(service.replaceWireColumnWithCondition(
  395. logic_id, rung_id, wire.id, 1,
  396. ContactNodeConfig{
  397. RegisterAddress{RegisterArea::M, 1},
  398. ContactMode::NormallyClosed})
  399. .succeeded,
  400. "a selected wire cell must be replaceable without removing adjacent cells");
  401. const LadderRung *rung = service.findRung(logic_id, rung_id);
  402. require(rung != nullptr && rung->condition.has_value()
  403. && rung->condition->kind == ConditionExpressionKind::Series
  404. && rung->condition->children.size() == 3U
  405. && rung->condition->children.front().wire->columnSpan == 1
  406. && rung->condition->children.at(1).kind
  407. == ConditionExpressionKind::Node
  408. && std::get<ContactNodeConfig>(
  409. rung->condition->children.at(1).node->config).mode
  410. == ContactMode::NormallyClosed
  411. && rung->condition->children.back().wire->columnSpan == 2
  412. && conditionColumns(*rung->condition) == 4,
  413. "wire-cell replacement must split the wire around the new contact");
  414. const std::vector<ControlLogic> before_invalid =
  415. project_service.project().controlLogics;
  416. require(service.replaceWireColumnWithCondition(
  417. logic_id,
  418. rung_id,
  419. rung->condition->children.front().id,
  420. 1,
  421. contact(2))
  422. .error == LogicEditorError::InvalidOperation,
  423. "an out-of-range wire-cell offset must be rejected");
  424. require(project_service.project().controlLogics.size() == before_invalid.size()
  425. && conditionColumns(*service.findRung(
  426. logic_id, rung_id)->condition) == 4,
  427. "a rejected wire-cell replacement must leave the network width unchanged");
  428. const LogicEditorResult first_cell_rung = service.addRung(logic_id);
  429. const LogicEditorResult first_cell_wire = service.appendWire(
  430. logic_id, first_cell_rung.id, 4);
  431. require(first_cell_rung.succeeded && first_cell_wire.succeeded
  432. && service.replaceWireColumnWithCondition(
  433. logic_id,
  434. first_cell_rung.id,
  435. first_cell_wire.id,
  436. 0,
  437. contact(10))
  438. .succeeded,
  439. "the first cell of a multi-column wire must be replaceable");
  440. const LadderRung *first_cell = service.findRung(logic_id, first_cell_rung.id);
  441. require(first_cell != nullptr && first_cell->condition.has_value()
  442. && first_cell->condition->kind == ConditionExpressionKind::Series
  443. && first_cell->condition->children.size() == 2U
  444. && first_cell->condition->children.front().kind
  445. == ConditionExpressionKind::Node
  446. && first_cell->condition->children.back().kind
  447. == ConditionExpressionKind::Wire
  448. && first_cell->condition->children.back().wire->columnSpan == 3,
  449. "first-cell replacement must preserve the trailing wire cells");
  450. require(service.undo().succeeded,
  451. "wire-cell replacement must be one undoable edit");
  452. const LadderRung *undone = service.findRung(logic_id, first_cell_rung.id);
  453. require(undone != nullptr && undone->condition.has_value()
  454. && undone->condition->kind == ConditionExpressionKind::Wire
  455. && undone->condition->wire->columnSpan == 4,
  456. "undo must restore the original unsplit wire");
  457. require(service.redo().succeeded,
  458. "wire-cell replacement must be redoable");
  459. const LadderRung *redone = service.findRung(logic_id, first_cell_rung.id);
  460. require(redone != nullptr && redone->condition.has_value()
  461. && redone->condition->kind == ConditionExpressionKind::Series
  462. && conditionColumns(*redone->condition) == 4,
  463. "redo must restore the split wire without changing its width");
  464. const std::string redone_trailing_wire_id =
  465. redone->condition->children.back().id;
  466. const LogicEditorResult last_cell_rung = service.addRung(logic_id);
  467. const LogicEditorResult last_cell_wire = service.appendWire(
  468. logic_id, last_cell_rung.id, 4);
  469. require(last_cell_rung.succeeded && last_cell_wire.succeeded
  470. && service.replaceWireColumnWithCondition(
  471. logic_id,
  472. last_cell_rung.id,
  473. last_cell_wire.id,
  474. 3,
  475. contact(11))
  476. .succeeded,
  477. "the last cell of a multi-column wire must be replaceable");
  478. const LadderRung *last_cell = service.findRung(logic_id, last_cell_rung.id);
  479. require(last_cell != nullptr && last_cell->condition.has_value()
  480. && last_cell->condition->kind == ConditionExpressionKind::Series
  481. && last_cell->condition->children.size() == 2U
  482. && last_cell->condition->children.front().kind
  483. == ConditionExpressionKind::Wire
  484. && last_cell->condition->children.front().wire->columnSpan == 3
  485. && last_cell->condition->children.back().kind
  486. == ConditionExpressionKind::Node,
  487. "last-cell replacement must preserve the leading wire cells");
  488. const LogicEditorResult single_cell_rung = service.addRung(logic_id);
  489. const LogicEditorResult single_cell_wire = service.appendWire(
  490. logic_id, single_cell_rung.id, 1);
  491. require(single_cell_rung.succeeded && single_cell_wire.succeeded
  492. && service.replaceWireColumnWithCondition(
  493. logic_id,
  494. single_cell_rung.id,
  495. single_cell_wire.id,
  496. 0,
  497. contact(12))
  498. .succeeded,
  499. "a one-column wire must use the same grid-cell replacement API");
  500. const LadderRung *single_cell = service.findRung(
  501. logic_id, single_cell_rung.id);
  502. require(single_cell != nullptr && single_cell->condition.has_value()
  503. && single_cell->condition->kind == ConditionExpressionKind::Node,
  504. "one-column wire replacement must normalize directly to a condition node");
  505. require(service.replaceWireColumnWithCondition(
  506. logic_id,
  507. first_cell_rung.id,
  508. redone_trailing_wire_id,
  509. 0,
  510. CoilNodeConfig{
  511. RegisterAddress{RegisterArea::M, 13}, CoilMode::Set})
  512. .error == LogicEditorError::InvalidNode,
  513. "wire grid cells must reject output instructions");
  514. require(service.replaceWireColumnWithCondition(
  515. logic_id,
  516. first_cell_rung.id,
  517. "missing-wire",
  518. 0,
  519. contact(13))
  520. .error == LogicEditorError::ExpressionNotFound,
  521. "wire grid replacement must reject an unknown wire without mutation");
  522. }
  523. void testSequentialConditionInsertionConsumesFollowingWire()
  524. {
  525. TestProjectStorage storage;
  526. ProjectService project_service(storage);
  527. LogicEditorService service(project_service);
  528. const std::string logic_id = service.ensureDefaultLogic().id;
  529. const std::string rung_id = service.firstRungId(logic_id);
  530. const LogicEditorResult wire = service.appendWire(logic_id, rung_id, 10);
  531. require(wire.succeeded,
  532. "sequential wire replacement must start with a full-width wire");
  533. LogicEditorResult inserted = service.replaceWireColumnWithCondition(
  534. logic_id, rung_id, wire.id, 0, contact(0));
  535. require(inserted.succeeded,
  536. "the first condition must replace the first full-wire cell");
  537. std::string selected_node_id = inserted.id;
  538. for (int address = 1; address < 10; ++address)
  539. {
  540. inserted = service.insertConditionAfter(
  541. logic_id, rung_id, selected_node_id, contact(address));
  542. require(inserted.succeeded,
  543. "continuous condition insertion must consume the following wire cell");
  544. selected_node_id = inserted.id;
  545. const LadderRung *rung = service.findRung(logic_id, rung_id);
  546. std::vector<const LogicNode *> nodes;
  547. collectConditionNodes(*rung->condition, &nodes);
  548. require(rung != nullptr && rung->condition.has_value()
  549. && conditionColumns(*rung->condition) == 10
  550. && nodes.size() == static_cast<std::size_t>(address + 1)
  551. && wireColumns(*rung->condition) == 9 - address,
  552. "each continuous insertion must preserve width while consuming one wire cell");
  553. }
  554. const LadderRung *full = service.findRung(logic_id, rung_id);
  555. require(full != nullptr && full->condition.has_value()
  556. && conditionColumns(*full->condition) == 10
  557. && wireColumns(*full->condition) == 0,
  558. "ten continuous insertions must replace the entire wire without expanding it");
  559. require(service.insertConditionAfter(
  560. logic_id, rung_id, selected_node_id, contact(10))
  561. .error == LogicEditorError::InvalidOperation,
  562. "the eleventh condition must still be rejected after all wire cells are consumed");
  563. require(conditionColumns(*service.findRung(
  564. logic_id, rung_id)->condition) == 10,
  565. "a rejected eleventh insertion must leave the full network unchanged");
  566. require(service.undo().succeeded,
  567. "a failed eleventh insertion must not displace the last successful undo step");
  568. const LadderRung *undone = service.findRung(logic_id, rung_id);
  569. std::vector<const LogicNode *> undone_nodes;
  570. collectConditionNodes(*undone->condition, &undone_nodes);
  571. require(undone_nodes.size() == 9U
  572. && wireColumns(*undone->condition) == 1
  573. && conditionColumns(*undone->condition) == 10,
  574. "undo must restore nine contacts followed by one wire cell");
  575. require(service.redo().succeeded,
  576. "the final wire-consuming insertion must be redoable");
  577. const LadderRung *redone = service.findRung(logic_id, rung_id);
  578. std::vector<const LogicNode *> redone_nodes;
  579. collectConditionNodes(*redone->condition, &redone_nodes);
  580. require(redone_nodes.size() == 10U
  581. && wireColumns(*redone->condition) == 0
  582. && conditionColumns(*redone->condition) == 10,
  583. "redo must restore all ten contacts without wire cells");
  584. }
  585. void testLogicLifecycleAndOrdering()
  586. {
  587. TestProjectStorage storage;
  588. ProjectService project_service(storage);
  589. LogicEditorService service(project_service);
  590. const std::string first_id = service.ensureDefaultLogic().id;
  591. const LogicEditorResult second = service.addLogic("Safety logic");
  592. const LogicEditorResult third = service.addLogic("Alarm logic");
  593. require(second.succeeded && third.succeeded,
  594. "multiple control logic modules must be creatable");
  595. require(service.renameLogic(second.id, "Interlock logic").succeeded,
  596. "control logic modules must be renamable by stable id");
  597. require(service.renameLogic(third.id, "Interlock logic").error
  598. == LogicEditorError::DuplicateName,
  599. "control logic names must remain unique");
  600. require(service.moveLogic(third.id, -1).succeeded
  601. && project_service.project().controlLogics.at(1).id == third.id,
  602. "logic scan order must follow editable vector order");
  603. require(service.setLogicEnabled(second.id, false).succeeded
  604. && !service.findLogic(second.id)->enabled,
  605. "a control logic module must support explicit disable and enable");
  606. require(service.setLogicEnabled(second.id, true).succeeded
  607. && service.findLogic(second.id)->enabled,
  608. "a disabled control logic module must be re-enableable");
  609. require(service.removeLogic(third.id).succeeded,
  610. "a non-final control logic module must be deletable");
  611. require(service.removeLogic(second.id).succeeded,
  612. "logic deletion must preserve the remaining module");
  613. require(service.removeLogic(first_id).error
  614. == LogicEditorError::LastLogicRequired,
  615. "the project must retain at least one control logic module");
  616. }
  617. void testEdgeTimerNodesAndRungComments()
  618. {
  619. TestProjectStorage storage;
  620. ProjectService project_service(storage);
  621. LogicEditorService service(project_service);
  622. const std::string logic_id = service.ensureDefaultLogic().id;
  623. const std::string rung_id = service.firstRungId(logic_id);
  624. const LogicEditorResult rising = service.appendCondition(
  625. logic_id,
  626. rung_id,
  627. EdgeContactNodeConfig{
  628. RegisterAddress{RegisterArea::M, 3}, EdgeMode::Rising});
  629. require(rising.succeeded && rising.id == "edge-1",
  630. "the editor must create rising edge nodes with a stable prefix");
  631. const LogicEditorResult timer = service.addParallelBranch(
  632. logic_id,
  633. rung_id,
  634. {rising.id},
  635. TimerContactNodeConfig{TimerAddress{2}, ContactMode::NormallyOpen});
  636. require(timer.succeeded && timer.id == "timer-contact-1",
  637. "the editor must insert T contacts as conditions");
  638. const LogicEditorResult ton = service.setOutput(
  639. logic_id, rung_id, TonNodeConfig{TimerAddress{2}, 500});
  640. require(ton.succeeded && ton.id == "ton-1",
  641. "the editor must create TON outputs with a stable prefix");
  642. require(service.updateNodeConfig(
  643. logic_id,
  644. rising.id,
  645. EdgeContactNodeConfig{
  646. RegisterAddress{RegisterArea::M, 4}, EdgeMode::Falling})
  647. .succeeded,
  648. "the editor must apply edge mode and M address properties");
  649. require(service.updateNodeConfig(
  650. logic_id,
  651. timer.id,
  652. TimerContactNodeConfig{TimerAddress{4}, ContactMode::NormallyClosed})
  653. .succeeded,
  654. "the editor must apply T contact mode and address properties");
  655. require(service.updateNodeConfig(
  656. logic_id,
  657. ton.id,
  658. TonNodeConfig{TimerAddress{4}, 750})
  659. .succeeded,
  660. "the editor must apply TON preset properties");
  661. require(service.updateRungComment(logic_id, rung_id, "延时启动网络").succeeded,
  662. "the editor must update a network comment by stable rung id");
  663. require(!service.updateRungComment(
  664. logic_id, rung_id, "第一行\n第二行").succeeded,
  665. "the editor must reject multiline network comments");
  666. require(!service.updateRungComment(
  667. logic_id,
  668. rung_id,
  669. std::string(ProjectLimits::kMaximumRungCommentBytes + 1U, 'a'))
  670. .succeeded,
  671. "the editor must reject oversized network comments");
  672. const LadderRung *rung = service.findRung(logic_id, rung_id);
  673. require(rung != nullptr && rung->comment == "延时启动网络"
  674. && std::get<EdgeContactNodeConfig>(
  675. rung->condition->children.front().node->config).mode
  676. == EdgeMode::Falling
  677. && std::get<TimerContactNodeConfig>(
  678. rung->condition->children.at(1).node->config).address
  679. == TimerAddress{4}
  680. && std::get<TonNodeConfig>(rung->output->config).presetMs == 750,
  681. "edge, T contact, TON and rung comment updates must remain in the model");
  682. }
  683. void testHistoryAndAtomicBatchDelete()
  684. {
  685. TestProjectStorage storage;
  686. ProjectService project_service(storage);
  687. LogicEditorService service(project_service);
  688. const std::string logic_id = service.ensureDefaultLogic().id;
  689. const std::string first_rung_id = service.firstRungId(logic_id);
  690. const LogicEditorResult first = service.appendCondition(
  691. logic_id, first_rung_id, contact(0));
  692. const LogicEditorResult second = service.appendCondition(
  693. logic_id, first_rung_id, contact(1));
  694. const LogicEditorResult second_rung = service.addRung(logic_id);
  695. const LogicEditorResult third = service.appendCondition(
  696. logic_id, second_rung.id, contact(2));
  697. require(first.succeeded && second.succeeded && second_rung.succeeded
  698. && third.succeeded,
  699. "nodes for history testing must be created");
  700. service.clearHistory();
  701. require(!service.removeNodes(logic_id, {first.id, "missing-node"}).succeeded,
  702. "batch node deletion must validate every id before changing the logic");
  703. require(service.findNode(logic_id, first.id) != nullptr
  704. && service.findNode(logic_id, third.id) != nullptr
  705. && !service.canUndo(),
  706. "failed batch node deletion must be atomic and leave history unchanged");
  707. require(service.removeNodes(logic_id, {first.id, third.id}).succeeded,
  708. "valid nodes across multiple rungs must be deleted together");
  709. require(service.findNode(logic_id, first.id) == nullptr
  710. && service.findNode(logic_id, third.id) == nullptr,
  711. "all selected nodes must be removed by one batch operation");
  712. require(service.undo().succeeded
  713. && service.findNode(logic_id, first.id) != nullptr
  714. && service.findNode(logic_id, third.id) != nullptr,
  715. "logic undo must restore a cross-rung batch deletion");
  716. require(service.redo().succeeded
  717. && service.findNode(logic_id, first.id) == nullptr
  718. && service.findNode(logic_id, third.id) == nullptr,
  719. "logic redo must reapply a cross-rung batch deletion");
  720. service.clearHistory();
  721. const ControlLogic *logic = service.findLogic(logic_id);
  722. require(logic != nullptr && service.setLogicEnabled(logic_id, logic->enabled).succeeded
  723. && !service.canUndo(),
  724. "setting an unchanged logic state must not consume history");
  725. service.clearHistory();
  726. for (int index = 1; index <= 101; ++index)
  727. {
  728. require(service.updateRungComment(
  729. logic_id, first_rung_id, "comment-" + std::to_string(index))
  730. .succeeded,
  731. "repeated valid rung edits must succeed");
  732. }
  733. int undo_count = 0;
  734. while (service.undo().succeeded)
  735. {
  736. ++undo_count;
  737. }
  738. require(undo_count == 100,
  739. "logic history must retain exactly the configured 100 most recent steps");
  740. require(service.redo().succeeded,
  741. "logic redo must be available after an undo");
  742. require(service.appendCondition(logic_id, first_rung_id, contact(5)).succeeded,
  743. "a new logic edit must succeed after undo");
  744. require(!service.canRedo(),
  745. "a new logic edit must clear the redo history");
  746. (void)second;
  747. }
  748. } // namespace
  749. int main()
  750. {
  751. try
  752. {
  753. testStructuredEditingAndNormalization();
  754. testRangeParallelInsertion();
  755. testStructuredWireEditing();
  756. testBatchDeleteAllNodesInParallelBranch();
  757. testConditionColumnLimit();
  758. testUnconditionalOutputEditing();
  759. testColumnTargetedConditionInsertion();
  760. testWireColumnReplacement();
  761. testSequentialConditionInsertionConsumesFollowingWire();
  762. testLogicLifecycleAndOrdering();
  763. testEdgeTimerNodesAndRungComments();
  764. testHistoryAndAtomicBatchDelete();
  765. }
  766. catch (const std::exception &error)
  767. {
  768. std::cerr << "logic editor service tests failed: " << error.what() << '\n';
  769. return 1;
  770. }
  771. std::cout << "logic editor service tests passed\n";
  772. return 0;
  773. }