综合平台编程器项目的远程存储
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

826 lines
38 KiB

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