综合平台编程器项目的远程存储
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.
 
 
 
 

1146 regels
55 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 "support/test_support.h"
  6. #include <algorithm>
  7. #include <iostream>
  8. #include <stdexcept>
  9. namespace {
  10. using TestProjectStorage = TestSupport::InMemoryProjectStorage;
  11. using TestSupport::require;
  12. ContactNodeConfig contact(int address)
  13. {
  14. return {RegisterAddress{RegisterArea::M, address}, ContactMode::NormallyOpen};
  15. }
  16. std::string makeEmptyRung(LogicEditorService &service, const std::string &logic_id)
  17. {
  18. const LogicEditorResult result = service.addRung(logic_id);
  19. require(result.succeeded, "test fixture must create an empty network");
  20. return result.id;
  21. }
  22. int conditionColumns(const ConditionExpression &expression)
  23. {
  24. if (expression.kind == ConditionExpressionKind::Node)
  25. {
  26. return 1;
  27. }
  28. if (expression.kind == ConditionExpressionKind::Wire)
  29. {
  30. return expression.wire->columnSpan;
  31. }
  32. int columns = expression.kind == ConditionExpressionKind::Series ? 0 : 1;
  33. for (const ConditionExpression &child : expression.children)
  34. {
  35. const int child_columns = conditionColumns(child);
  36. columns = expression.kind == ConditionExpressionKind::Series
  37. ? columns + child_columns : std::max(columns, child_columns);
  38. }
  39. return columns;
  40. }
  41. int wireColumns(const ConditionExpression &expression)
  42. {
  43. if (expression.kind == ConditionExpressionKind::Node)
  44. {
  45. return 0;
  46. }
  47. if (expression.kind == ConditionExpressionKind::Wire)
  48. {
  49. return expression.wire->columnSpan;
  50. }
  51. int columns = 0;
  52. for (const ConditionExpression &child : expression.children)
  53. {
  54. columns += wireColumns(child);
  55. }
  56. return columns;
  57. }
  58. int conditionNodes(const ConditionExpression &expression)
  59. {
  60. if (expression.kind == ConditionExpressionKind::Node)
  61. {
  62. return 1;
  63. }
  64. if (expression.kind == ConditionExpressionKind::Wire)
  65. {
  66. return 0;
  67. }
  68. int count = 0;
  69. for (const ConditionExpression &child : expression.children)
  70. {
  71. count += conditionNodes(child);
  72. }
  73. return count;
  74. }
  75. void testEmptyLogicCreatesNetworksOnFirstEdit()
  76. {
  77. TestProjectStorage storage;
  78. ProjectService project_service(storage);
  79. LogicEditorService service(project_service);
  80. const std::string logic_id = service.ensureDefaultLogic().id;
  81. require(service.findLogic(logic_id)->rungs.empty(),
  82. "a default control logic must start without an empty network");
  83. const LogicEditorResult first_condition = service.appendCondition(
  84. logic_id, {}, contact(0));
  85. require(first_condition.succeeded
  86. && service.findLogic(logic_id)->rungs.size() == 1U,
  87. "the first condition must create network 1 atomically");
  88. const LadderRung &condition_rung = service.findLogic(logic_id)->rungs.front();
  89. require(condition_rung.condition.has_value()
  90. && condition_rung.condition->kind == ConditionExpressionKind::Node,
  91. "the first condition must not create an implicit horizontal wire");
  92. require(service.removeRung(logic_id, condition_rung.id).succeeded
  93. && service.findLogic(logic_id)->rungs.empty(),
  94. "the last network must be removable back to an empty logic");
  95. const LogicEditorResult first_wire = service.appendWire(logic_id, {}, 1);
  96. require(first_wire.succeeded
  97. && service.findLogic(logic_id)->rungs.size() == 1U
  98. && service.findLogic(logic_id)->rungs.front().condition->kind
  99. == ConditionExpressionKind::Wire,
  100. "the first horizontal wire must create a one-cell network");
  101. require(service.removeRung(
  102. logic_id, service.findLogic(logic_id)->rungs.front().id).succeeded,
  103. "the wire-only network must be removable");
  104. const LogicEditorResult first_output = service.setOutput(
  105. logic_id,
  106. {},
  107. CoilNodeConfig{RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal});
  108. require(first_output.succeeded
  109. && service.findLogic(logic_id)->rungs.size() == 1U
  110. && !service.findLogic(logic_id)->rungs.front().condition.has_value()
  111. && service.findLogic(logic_id)->rungs.front().output.has_value(),
  112. "the first output must create an unconditional network");
  113. }
  114. void testAppendingWireMovesToNextNetworkAfterTenColumns()
  115. {
  116. TestProjectStorage storage;
  117. ProjectService project_service(storage);
  118. LogicEditorService service(project_service);
  119. const std::string logic_id = service.ensureDefaultLogic().id;
  120. const std::string rung_id = makeEmptyRung(service, logic_id);
  121. for (int index = 0; index < ProjectLimits::kMaximumConditionColumns; ++index)
  122. {
  123. require(service.appendWire(logic_id, rung_id).succeeded,
  124. "ten horizontal wire cells must fit in one network");
  125. }
  126. const LogicEditorResult next = service.appendWire(logic_id, rung_id);
  127. require(next.succeeded && service.findLogic(logic_id)->rungs.size() == 2U,
  128. "the next appended wire must create the following network");
  129. const std::string next_rung_id = service.findLogic(logic_id)->rungs.back().id;
  130. require(service.findRung(logic_id, rung_id)->condition.has_value()
  131. && conditionColumns(*service.findRung(logic_id, rung_id)->condition)
  132. == ProjectLimits::kMaximumConditionColumns
  133. && service.findRung(logic_id, next_rung_id)->condition.has_value(),
  134. "automatic network rollover must preserve both network contents");
  135. require(service.undo().succeeded && service.findLogic(logic_id)->rungs.size() == 1U,
  136. "automatic network rollover must be undone as one edit");
  137. }
  138. void testStructuredEditingAndNormalization()
  139. {
  140. TestProjectStorage storage;
  141. ProjectService project_service(storage);
  142. LogicEditorService service(project_service);
  143. const std::string logic_id = service.ensureDefaultLogic().id;
  144. const std::string rung_id = makeEmptyRung(service, logic_id);
  145. const LogicEditorResult first = service.appendCondition(logic_id, rung_id, contact(0));
  146. const LogicEditorResult second = service.appendCondition(logic_id, rung_id, contact(1));
  147. require(first.succeeded && second.succeeded, "series append must succeed");
  148. const LadderRung *rung = service.findRung(logic_id, rung_id);
  149. require(rung->condition->kind == ConditionExpressionKind::Series
  150. && rung->condition->children.size() == 2U,
  151. "two appended nodes must form a series expression");
  152. const std::string second_expression_id = second.id;
  153. const LogicEditorResult parallel = service.addParallelBranch(
  154. logic_id, rung_id, {second_expression_id}, contact(2));
  155. require(parallel.succeeded, "parallel insertion must succeed");
  156. rung = service.findRung(logic_id, rung_id);
  157. const ConditionExpression *parallel_expression = service.findExpression(
  158. logic_id, rung_id, rung->condition->children.at(1).id);
  159. require(parallel_expression != nullptr
  160. && parallel_expression->kind == ConditionExpressionKind::Parallel,
  161. "selected node must become a parallel expression");
  162. const LogicEditorResult nested_series = service.insertConditionAfter(
  163. logic_id,
  164. rung_id,
  165. parallel.id,
  166. contact(3));
  167. require(nested_series.succeeded, "a parallel branch must accept a series node");
  168. rung = service.findRung(logic_id, rung_id);
  169. require(rung->condition->kind == ConditionExpressionKind::Series,
  170. "root must remain a series expression");
  171. const ConditionExpression &nested_parallel_expression = rung->condition->children.at(1);
  172. require(nested_parallel_expression.kind == ConditionExpressionKind::Parallel
  173. && nested_parallel_expression.children.at(1).kind
  174. == ConditionExpressionKind::Series,
  175. "editor must express A AND (B OR (C AND D))");
  176. require(service.removeNode(logic_id, nested_series.id).succeeded,
  177. "nested series node deletion must succeed");
  178. rung = service.findRung(logic_id, rung_id);
  179. require(rung->condition->children.at(1).kind == ConditionExpressionKind::Parallel
  180. && rung->condition->children.at(1).children.at(1).kind
  181. == ConditionExpressionKind::Node,
  182. "single-child series container must collapse after deletion");
  183. require(service.removeNode(logic_id, parallel.id).succeeded,
  184. "parallel leaf deletion must succeed");
  185. rung = service.findRung(logic_id, rung_id);
  186. require(rung->condition->kind == ConditionExpressionKind::Series
  187. && rung->condition->children.size() == 2U,
  188. "single-child parallel container must collapse after deletion");
  189. require(service.setOutput(
  190. logic_id,
  191. rung_id,
  192. CoilNodeConfig{RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal})
  193. .succeeded,
  194. "output coil must be set");
  195. require(service.findLogic(logic_id)->validate(),
  196. "structured editing result must remain a valid draft");
  197. }
  198. void testRangeParallelInsertion()
  199. {
  200. TestProjectStorage storage;
  201. ProjectService project_service(storage);
  202. LogicEditorService service(project_service);
  203. const std::string logic_id = service.ensureDefaultLogic().id;
  204. const std::string rung_id = makeEmptyRung(service, logic_id);
  205. service.appendCondition(logic_id, rung_id, contact(0));
  206. service.appendCondition(logic_id, rung_id, contact(1));
  207. service.appendCondition(logic_id, rung_id, contact(2));
  208. const LogicEditorResult branch = service.addParallelBranch(
  209. logic_id, rung_id, {"contact-2", "contact-3"}, contact(3));
  210. require(branch.succeeded, "a continuous series range must accept a parallel branch");
  211. const ConditionExpression &root = *service.findRung(logic_id, rung_id)->condition;
  212. require(root.kind == ConditionExpressionKind::Series
  213. && root.children.size() == 2U
  214. && root.children.at(1).kind == ConditionExpressionKind::Parallel
  215. && root.children.at(1).children.front().kind
  216. == ConditionExpressionKind::Series,
  217. "range insertion must express A AND ((B AND C) OR D)");
  218. require(root.validate(), "range insertion must preserve normalized topology");
  219. const LogicEditorResult invalid = service.addParallelBranch(
  220. logic_id, rung_id, {"contact-1", "contact-3"}, contact(4));
  221. require(!invalid.succeeded
  222. && invalid.error == LogicEditorError::InvalidOperation,
  223. "a non-contiguous selection must be rejected");
  224. }
  225. void testParallelBranchGridInsertion()
  226. {
  227. TestProjectStorage storage;
  228. ProjectService project_service(storage);
  229. LogicEditorService service(project_service);
  230. const std::string logic_id = service.ensureDefaultLogic().id;
  231. const std::string rung_id = makeEmptyRung(service, logic_id);
  232. const LogicEditorResult first = service.appendCondition(
  233. logic_id, rung_id, contact(0));
  234. const LogicEditorResult second = service.appendCondition(
  235. logic_id, rung_id, contact(1));
  236. const LogicEditorResult third = service.appendCondition(
  237. logic_id, rung_id, contact(2));
  238. const LogicEditorResult branch = service.addParallelBranch(
  239. logic_id, rung_id, {first.id, second.id, third.id}, contact(10));
  240. require(branch.succeeded,
  241. "parallel grid insertion setup must create a short lower branch");
  242. service.clearHistory();
  243. const LogicEditorResult inserted = service.insertConditionInBranchAtColumn(
  244. logic_id, rung_id, branch.id, 2, contact(12));
  245. require(inserted.succeeded,
  246. "a visible parallel branch padding cell must accept a condition");
  247. const LadderRung *rung = service.findRung(logic_id, rung_id);
  248. require(rung != nullptr && rung->condition.has_value()
  249. && rung->condition->kind == ConditionExpressionKind::Parallel,
  250. "branch grid insertion must preserve the surrounding parallel expression");
  251. const ConditionExpression &lower = rung->condition->children.at(1);
  252. require(lower.kind == ConditionExpressionKind::Series
  253. && lower.children.size() == 3U
  254. && lower.children.at(0).node->id == branch.id
  255. && lower.children.at(1).kind == ConditionExpressionKind::Wire
  256. && lower.children.at(1).wire->columnSpan == 1
  257. && lower.children.at(2).node->id == inserted.id,
  258. "a distant branch cell must persist only the required gap and new condition");
  259. require(service.undo().succeeded,
  260. "parallel branch grid insertion must be one undoable edit");
  261. rung = service.findRung(logic_id, rung_id);
  262. require(rung->condition->children.at(1).kind == ConditionExpressionKind::Node
  263. && rung->condition->children.at(1).node->id == branch.id,
  264. "undo must restore the original short parallel branch");
  265. service.clearHistory();
  266. const bool modified_before = project_service.isModified();
  267. const LogicEditorResult invalid = service.insertConditionInBranchAtColumn(
  268. logic_id, rung_id, branch.id, 3, contact(13));
  269. require(!invalid.succeeded && !service.canUndo()
  270. && project_service.isModified() == modified_before
  271. && service.findNode(logic_id, inserted.id) == nullptr,
  272. "a cell outside the visible branch padding must fail atomically");
  273. }
  274. void testStructuredWireEditing()
  275. {
  276. TestProjectStorage storage;
  277. ProjectService project_service(storage);
  278. LogicEditorService service(project_service);
  279. const std::string logic_id = service.ensureDefaultLogic().id;
  280. const std::string rung_id = makeEmptyRung(service, logic_id);
  281. service.appendCondition(logic_id, rung_id, contact(0));
  282. service.appendCondition(logic_id, rung_id, contact(1));
  283. service.appendCondition(logic_id, rung_id, contact(2));
  284. const LogicEditorResult branch = service.addParallelWireBranch(
  285. logic_id, rung_id, {"contact-2", "contact-3"});
  286. require(branch.succeeded && branch.id == "wire-1",
  287. "a continuous range must accept a structured horizontal bypass");
  288. const LadderRung *rung = service.findRung(logic_id, rung_id);
  289. require(rung->condition->kind == ConditionExpressionKind::Series
  290. && rung->condition->children.at(1).kind
  291. == ConditionExpressionKind::Parallel,
  292. "a vertical connection must produce a parallel expression");
  293. const ConditionExpression &wire =
  294. rung->condition->children.at(1).children.at(1);
  295. require(wire.kind == ConditionExpressionKind::Wire
  296. && wire.wire->columnSpan == 2,
  297. "the bypass wire span must match the selected two-column range");
  298. const LogicEditorResult replacement = service.replaceWireWithCondition(
  299. logic_id, rung_id, branch.id, contact(3));
  300. require(replacement.succeeded && replacement.id == "contact-4",
  301. "a selected wire must be replaceable by a configured node type");
  302. const LogicEditorResult extension = service.insertWireAfter(
  303. logic_id, rung_id, replacement.id);
  304. require(extension.succeeded && extension.id == "wire-1",
  305. "a wire id must become reusable after its expression is replaced");
  306. rung = service.findRung(logic_id, rung_id);
  307. const ConditionExpression *extended_branch = service.findExpression(
  308. logic_id, rung_id, rung->condition->children.at(1).children.at(1).id);
  309. require(extended_branch != nullptr
  310. && extended_branch->kind == ConditionExpressionKind::Series
  311. && extended_branch->children.at(1).kind
  312. == ConditionExpressionKind::Wire,
  313. "inserting a horizontal wire after a branch node must preserve structure");
  314. const std::string extended_branch_id = extended_branch->id;
  315. require(!service.addParallelWireBranch(
  316. logic_id, rung_id, {"contact-1", "contact-3"}).succeeded,
  317. "non-contiguous wire connection targets must be rejected");
  318. require(service.removeExpressions(
  319. logic_id, rung_id, {extended_branch_id}).succeeded,
  320. "deleting a selected vertical connection branch must succeed atomically");
  321. rung = service.findRung(logic_id, rung_id);
  322. require(rung->condition->kind == ConditionExpressionKind::Series
  323. && rung->condition->children.size() == 3U,
  324. "removing a bypass branch must normalize back to the original series");
  325. require(service.undo().succeeded
  326. && service.findExpression(logic_id, rung_id, extended_branch_id) != nullptr,
  327. "wire branch deletion must participate in ladder undo history");
  328. }
  329. void testWireCellParallelSelectionUsesExactColumns()
  330. {
  331. TestProjectStorage storage;
  332. ProjectService project_service(storage);
  333. LogicEditorService service(project_service);
  334. const std::string logic_id = service.ensureDefaultLogic().id;
  335. const std::string rung_id = makeEmptyRung(service, logic_id);
  336. const LogicEditorResult source = service.appendWire(
  337. logic_id, rung_id, 3);
  338. require(source.succeeded, "wire-cell parallel setup must create a three-column wire");
  339. const LogicEditorResult branch = service.addParallelWireBranchAtCells(
  340. logic_id,
  341. rung_id,
  342. {{source.id, 0}, {source.id, 1}});
  343. require(branch.succeeded,
  344. "a selected two-cell wire range must create a parallel bypass");
  345. const LadderRung *rung = service.findRung(logic_id, rung_id);
  346. require(rung != nullptr && rung->condition.has_value()
  347. && rung->condition->kind == ConditionExpressionKind::Series
  348. && rung->condition->children.size() == 2U,
  349. "a partial wire selection must preserve the surrounding series layout");
  350. const ConditionExpression &parallel = rung->condition->children.front();
  351. require(parallel.kind == ConditionExpressionKind::Parallel
  352. && parallel.children.size() == 2U
  353. && parallel.children.front().kind == ConditionExpressionKind::Wire
  354. && parallel.children.front().wire->columnSpan == 2
  355. && parallel.children.back().kind == ConditionExpressionKind::Wire
  356. && parallel.children.back().wire->columnSpan == 2
  357. && rung->condition->children.back().kind
  358. == ConditionExpressionKind::Wire
  359. && rung->condition->children.back().wire->columnSpan == 1
  360. && conditionColumns(*rung->condition) == 3,
  361. "the new bypass width must match the selected two cells, not the full source wire");
  362. service.clearHistory();
  363. const bool modified_before = project_service.isModified();
  364. const LogicEditorResult non_contiguous =
  365. service.addParallelWireBranchAtCells(
  366. logic_id,
  367. rung_id,
  368. {{source.id, 0}, {source.id, 2}});
  369. require(!non_contiguous.succeeded
  370. && project_service.isModified() == modified_before
  371. && !service.canUndo(),
  372. "non-contiguous wire-cell selections must fail without mutation");
  373. }
  374. void testWireCellParallelSelectionAcrossAdjacentWires()
  375. {
  376. TestProjectStorage storage;
  377. ProjectService project_service(storage);
  378. LogicEditorService service(project_service);
  379. const std::string logic_id = service.ensureDefaultLogic().id;
  380. const std::string rung_id = makeEmptyRung(service, logic_id);
  381. std::vector<std::string> wire_ids;
  382. for (int index = 0; index < 3; ++index)
  383. {
  384. const LogicEditorResult wire = service.appendWire(logic_id, rung_id);
  385. require(wire.succeeded,
  386. "adjacent wire setup must create each one-column segment");
  387. wire_ids.push_back(wire.id);
  388. }
  389. const LogicEditorResult branch = service.addParallelWireBranchAtCells(
  390. logic_id,
  391. rung_id,
  392. {{wire_ids.at(0), 0}, {wire_ids.at(1), 0}, {wire_ids.at(2), 0}});
  393. require(branch.succeeded,
  394. "visually continuous adjacent wire cells must create one bypass");
  395. const LadderRung *rung = service.findRung(logic_id, rung_id);
  396. require(rung != nullptr && rung->condition.has_value()
  397. && rung->condition->kind == ConditionExpressionKind::Parallel
  398. && rung->condition->children.size() == 2U
  399. && rung->condition->children.front().kind
  400. == ConditionExpressionKind::Wire
  401. && rung->condition->children.front().wire->columnSpan == 3
  402. && rung->condition->children.back().kind
  403. == ConditionExpressionKind::Wire
  404. && rung->condition->children.back().wire->columnSpan == 3,
  405. "three adjacent one-column wires must become a three-column parallel range");
  406. }
  407. void testBatchDeleteAllNodesInParallelBranch()
  408. {
  409. TestProjectStorage storage;
  410. ProjectService project_service(storage);
  411. LogicEditorService service(project_service);
  412. const std::string logic_id = service.ensureDefaultLogic().id;
  413. const std::string rung_id = makeEmptyRung(service, logic_id);
  414. const LogicEditorResult first = service.appendCondition(
  415. logic_id, rung_id, contact(0));
  416. const LogicEditorResult second = service.appendCondition(
  417. logic_id, rung_id, contact(1));
  418. const LogicEditorResult third = service.appendCondition(
  419. logic_id, rung_id, contact(2));
  420. const LogicEditorResult fourth = service.appendCondition(
  421. logic_id, rung_id, contact(3));
  422. require(first.succeeded && second.succeeded && third.succeeded
  423. && fourth.succeeded,
  424. "parallel batch deletion setup contacts must be created");
  425. const LogicEditorResult branch = service.addParallelBranch(
  426. logic_id, rung_id,
  427. {second.id, third.id, fourth.id},
  428. contact(10));
  429. require(branch.succeeded,
  430. "parallel batch deletion setup branch must be created");
  431. require(service.removeNodes(
  432. logic_id, {second.id, third.id, fourth.id})
  433. .succeeded,
  434. "deleting every node in a parallel branch as one batch must succeed");
  435. const LadderRung *rung = service.findRung(logic_id, rung_id);
  436. require(rung != nullptr && rung->condition.has_value()
  437. && rung->validate(),
  438. "batch deletion must leave a valid normalized ladder expression");
  439. require(rung->condition->kind == ConditionExpressionKind::Series
  440. && rung->condition->children.size() == 2U
  441. && rung->condition->children.at(0).kind
  442. == ConditionExpressionKind::Node
  443. && rung->condition->children.at(1).kind
  444. == ConditionExpressionKind::Node,
  445. "an empty parallel branch must collapse into the remaining branch");
  446. require(service.findNode(logic_id, second.id) == nullptr
  447. && service.findNode(logic_id, third.id) == nullptr
  448. && service.findNode(logic_id, fourth.id) == nullptr,
  449. "all selected parallel branch nodes must be removed");
  450. require(service.undo().succeeded,
  451. "parallel batch deletion must be undoable");
  452. require(service.findNode(logic_id, second.id) != nullptr
  453. && service.findNode(logic_id, third.id) != nullptr
  454. && service.findNode(logic_id, fourth.id) != nullptr,
  455. "undo must restore every deleted parallel branch node");
  456. }
  457. void testConditionColumnLimit()
  458. {
  459. TestProjectStorage storage;
  460. ProjectService project_service(storage);
  461. LogicEditorService service(project_service);
  462. const std::string logic_id = service.ensureDefaultLogic().id;
  463. const std::string rung_id = makeEmptyRung(service, logic_id);
  464. for (int column = 0; column < ProjectLimits::kMaximumConditionColumns; ++column)
  465. {
  466. require(service.appendCondition(logic_id, rung_id, contact(column)).succeeded,
  467. "the first ten condition columns must be editable");
  468. }
  469. require(project_service.saveAs("logic-editor-condition-limit.json").succeeded,
  470. "the ten-column network must be saveable before testing overflow");
  471. require(!project_service.isModified(),
  472. "saving the ten-column network must clear the modified state");
  473. const LogicEditorResult overflow = service.appendCondition(
  474. logic_id, rung_id, contact(ProjectLimits::kMaximumConditionColumns));
  475. require(!overflow.succeeded
  476. && overflow.error == LogicEditorError::InvalidOperation,
  477. "the eleventh condition column must be rejected by the editor service");
  478. require(!project_service.isModified(),
  479. "a failed eleventh-column edit must preserve the saved state");
  480. const LadderRung *rung = service.findRung(logic_id, rung_id);
  481. require(rung != nullptr && rung->condition.has_value()
  482. && rung->condition->kind == ConditionExpressionKind::Series
  483. && rung->condition->children.size()
  484. == static_cast<std::size_t>(
  485. ProjectLimits::kMaximumConditionColumns),
  486. "a rejected eleventh column must leave the ten-column network unchanged");
  487. require(service.setOutput(
  488. logic_id,
  489. rung_id,
  490. CoilNodeConfig{
  491. RegisterAddress{RegisterArea::M, 20}, CoilMode::Normal},
  492. true)
  493. .succeeded
  494. && project_service.isModified(),
  495. "a successful edit must make the project modified again");
  496. require(!service.appendCondition(
  497. logic_id, rung_id,
  498. contact(ProjectLimits::kMaximumConditionColumns))
  499. .succeeded
  500. && project_service.isModified(),
  501. "a failed edit must preserve an existing modified state");
  502. }
  503. void testUnconditionalOutputEditing()
  504. {
  505. TestProjectStorage storage;
  506. ProjectService project_service(storage);
  507. LogicEditorService service(project_service);
  508. const std::string logic_id = service.ensureDefaultLogic().id;
  509. const std::string rung_id = makeEmptyRung(service, logic_id);
  510. require(service.setOutput(
  511. logic_id,
  512. rung_id,
  513. CoilNodeConfig{
  514. RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal},
  515. true)
  516. .succeeded,
  517. "the editor must allow a coil before any condition is added");
  518. const LadderRung *rung = service.findRung(logic_id, rung_id);
  519. require(rung != nullptr && !rung->condition.has_value()
  520. && rung->output.has_value() && rung->validateForRunning(),
  521. "an editor-created output-only network must be runnable as unconditional");
  522. }
  523. void testColumnTargetedConditionInsertion()
  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 first_rung_id = makeEmptyRung(service, logic_id);
  530. require(service.insertConditionAtColumn(
  531. logic_id, first_rung_id, 4, contact(4)).succeeded,
  532. "an empty network must accept a condition at the selected fifth column");
  533. const LadderRung *rung = service.findRung(logic_id, first_rung_id);
  534. require(rung != nullptr && rung->condition.has_value()
  535. && rung->condition->kind == ConditionExpressionKind::Series
  536. && rung->condition->children.size() == 2U
  537. && rung->condition->children.front().kind
  538. == ConditionExpressionKind::Wire
  539. && rung->condition->children.front().wire->columnSpan == 4
  540. && rung->condition->children.back().kind
  541. == ConditionExpressionKind::Node
  542. && rung->validate()
  543. && conditionColumns(*rung->condition) == 5,
  544. "column insertion must preserve the requested horizontal position");
  545. require(service.insertConditionAtColumn(
  546. logic_id, first_rung_id, 2, contact(2)).error
  547. == LogicEditorError::InvalidOperation,
  548. "inserting into an already occupied column must be rejected atomically");
  549. require(service.insertConditionAtColumn(
  550. logic_id, first_rung_id, 10, contact(10)).error
  551. == LogicEditorError::InvalidOperation,
  552. "the eleventh condition column must be rejected");
  553. require(service.insertConditionAtColumn(
  554. logic_id, first_rung_id, -1, contact(10)).error
  555. == LogicEditorError::InvalidOperation,
  556. "a negative grid column must be rejected");
  557. require(service.insertConditionAtColumn(
  558. logic_id,
  559. first_rung_id,
  560. 5,
  561. CoilNodeConfig{
  562. RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal})
  563. .error == LogicEditorError::InvalidNode,
  564. "a condition grid slot must reject output instructions");
  565. const LogicEditorResult second_rung = service.addRung(logic_id);
  566. require(second_rung.succeeded,
  567. "column insertion test must create a second empty network");
  568. require(service.setOutput(
  569. logic_id,
  570. second_rung.id,
  571. CoilNodeConfig{RegisterAddress{RegisterArea::M, 20}, CoilMode::Normal},
  572. true)
  573. .succeeded,
  574. "an output-only network must be configurable before adding a condition");
  575. require(service.insertConditionAtColumn(
  576. logic_id, second_rung.id, 0, contact(0)).succeeded,
  577. "a selected first grid slot must work on an output-only network");
  578. const LadderRung *output_rung = service.findRung(logic_id, second_rung.id);
  579. require(output_rung != nullptr && output_rung->condition.has_value()
  580. && output_rung->condition->kind == ConditionExpressionKind::Node
  581. && output_rung->output.has_value()
  582. && output_rung->validate(),
  583. "condition insertion must keep the independent output slot intact");
  584. const LogicEditorResult last_column_rung = service.addRung(logic_id);
  585. require(last_column_rung.succeeded
  586. && service.insertConditionAtColumn(
  587. logic_id, last_column_rung.id, 9, contact(9)).succeeded,
  588. "the tenth condition column must remain a valid insertion target");
  589. const LadderRung *full_width = service.findRung(
  590. logic_id, last_column_rung.id);
  591. require(full_width != nullptr && full_width->condition.has_value()
  592. && full_width->condition->kind == ConditionExpressionKind::Series
  593. && full_width->condition->children.front().wire->columnSpan == 9
  594. && conditionColumns(*full_width->condition) == 10,
  595. "last-column insertion must fill exactly the ten-column condition area");
  596. require(!service.appendCondition(
  597. logic_id, last_column_rung.id, contact(10)).succeeded
  598. && conditionColumns(*service.findRung(
  599. logic_id, last_column_rung.id)->condition) == 10,
  600. "a full-width grid must reject another condition without partial changes");
  601. }
  602. void testWireColumnReplacement()
  603. {
  604. TestProjectStorage storage;
  605. ProjectService project_service(storage);
  606. LogicEditorService service(project_service);
  607. const std::string logic_id = service.ensureDefaultLogic().id;
  608. const std::string rung_id = makeEmptyRung(service, logic_id);
  609. const LogicEditorResult wire = service.appendWire(logic_id, rung_id, 4);
  610. require(wire.succeeded,
  611. "wire-column replacement test must create a four-column wire");
  612. require(service.replaceWireColumnWithCondition(
  613. logic_id, rung_id, wire.id, 1,
  614. ContactNodeConfig{
  615. RegisterAddress{RegisterArea::M, 1},
  616. ContactMode::NormallyClosed})
  617. .succeeded,
  618. "a selected wire cell must be replaceable without removing adjacent cells");
  619. const LadderRung *rung = service.findRung(logic_id, rung_id);
  620. require(rung != nullptr && rung->condition.has_value()
  621. && rung->condition->kind == ConditionExpressionKind::Series
  622. && rung->condition->children.size() == 3U
  623. && rung->condition->children.front().wire->columnSpan == 1
  624. && rung->condition->children.at(1).kind
  625. == ConditionExpressionKind::Node
  626. && std::get<ContactNodeConfig>(
  627. rung->condition->children.at(1).node->config).mode
  628. == ContactMode::NormallyClosed
  629. && rung->condition->children.back().wire->columnSpan == 2
  630. && conditionColumns(*rung->condition) == 4,
  631. "wire-cell replacement must split the wire around the new contact");
  632. const std::vector<ControlLogic> before_invalid =
  633. project_service.project().controlLogics;
  634. require(service.replaceWireColumnWithCondition(
  635. logic_id,
  636. rung_id,
  637. rung->condition->children.front().id,
  638. 1,
  639. contact(2))
  640. .error == LogicEditorError::InvalidOperation,
  641. "an out-of-range wire-cell offset must be rejected");
  642. require(project_service.project().controlLogics.size() == before_invalid.size()
  643. && conditionColumns(*service.findRung(
  644. logic_id, rung_id)->condition) == 4,
  645. "a rejected wire-cell replacement must leave the network width unchanged");
  646. const LogicEditorResult first_cell_rung = service.addRung(logic_id);
  647. const LogicEditorResult first_cell_wire = service.appendWire(
  648. logic_id, first_cell_rung.id, 4);
  649. require(first_cell_rung.succeeded && first_cell_wire.succeeded
  650. && service.replaceWireColumnWithCondition(
  651. logic_id,
  652. first_cell_rung.id,
  653. first_cell_wire.id,
  654. 0,
  655. contact(10))
  656. .succeeded,
  657. "the first cell of a multi-column wire must be replaceable");
  658. const LadderRung *first_cell = service.findRung(logic_id, first_cell_rung.id);
  659. require(first_cell != nullptr && first_cell->condition.has_value()
  660. && first_cell->condition->kind == ConditionExpressionKind::Series
  661. && first_cell->condition->children.size() == 2U
  662. && first_cell->condition->children.front().kind
  663. == ConditionExpressionKind::Node
  664. && first_cell->condition->children.back().kind
  665. == ConditionExpressionKind::Wire
  666. && first_cell->condition->children.back().wire->columnSpan == 3,
  667. "first-cell replacement must preserve the trailing wire cells");
  668. require(service.undo().succeeded,
  669. "wire-cell replacement must be one undoable edit");
  670. const LadderRung *undone = service.findRung(logic_id, first_cell_rung.id);
  671. require(undone != nullptr && undone->condition.has_value()
  672. && undone->condition->kind == ConditionExpressionKind::Wire
  673. && undone->condition->wire->columnSpan == 4,
  674. "undo must restore the original unsplit wire");
  675. require(service.redo().succeeded,
  676. "wire-cell replacement must be redoable");
  677. const LadderRung *redone = service.findRung(logic_id, first_cell_rung.id);
  678. require(redone != nullptr && redone->condition.has_value()
  679. && redone->condition->kind == ConditionExpressionKind::Series
  680. && conditionColumns(*redone->condition) == 4,
  681. "redo must restore the split wire without changing its width");
  682. const std::string redone_trailing_wire_id =
  683. redone->condition->children.back().id;
  684. const LogicEditorResult last_cell_rung = service.addRung(logic_id);
  685. const LogicEditorResult last_cell_wire = service.appendWire(
  686. logic_id, last_cell_rung.id, 4);
  687. require(last_cell_rung.succeeded && last_cell_wire.succeeded
  688. && service.replaceWireColumnWithCondition(
  689. logic_id,
  690. last_cell_rung.id,
  691. last_cell_wire.id,
  692. 3,
  693. contact(11))
  694. .succeeded,
  695. "the last cell of a multi-column wire must be replaceable");
  696. const LadderRung *last_cell = service.findRung(logic_id, last_cell_rung.id);
  697. require(last_cell != nullptr && last_cell->condition.has_value()
  698. && last_cell->condition->kind == ConditionExpressionKind::Series
  699. && last_cell->condition->children.size() == 2U
  700. && last_cell->condition->children.front().kind
  701. == ConditionExpressionKind::Wire
  702. && last_cell->condition->children.front().wire->columnSpan == 3
  703. && last_cell->condition->children.back().kind
  704. == ConditionExpressionKind::Node,
  705. "last-cell replacement must preserve the leading wire cells");
  706. const LogicEditorResult single_cell_rung = service.addRung(logic_id);
  707. const LogicEditorResult single_cell_wire = service.appendWire(
  708. logic_id, single_cell_rung.id, 1);
  709. require(single_cell_rung.succeeded && single_cell_wire.succeeded
  710. && service.replaceWireColumnWithCondition(
  711. logic_id,
  712. single_cell_rung.id,
  713. single_cell_wire.id,
  714. 0,
  715. contact(12))
  716. .succeeded,
  717. "a one-column wire must use the same grid-cell replacement API");
  718. const LadderRung *single_cell = service.findRung(
  719. logic_id, single_cell_rung.id);
  720. require(single_cell != nullptr && single_cell->condition.has_value()
  721. && single_cell->condition->kind == ConditionExpressionKind::Node,
  722. "one-column wire replacement must normalize directly to a condition node");
  723. require(service.replaceWireColumnWithCondition(
  724. logic_id,
  725. first_cell_rung.id,
  726. redone_trailing_wire_id,
  727. 0,
  728. CoilNodeConfig{
  729. RegisterAddress{RegisterArea::M, 13}, CoilMode::Set})
  730. .error == LogicEditorError::InvalidNode,
  731. "wire grid cells must reject output instructions");
  732. require(service.replaceWireColumnWithCondition(
  733. logic_id,
  734. first_cell_rung.id,
  735. "missing-wire",
  736. 0,
  737. contact(13))
  738. .error == LogicEditorError::ExpressionNotFound,
  739. "wire grid replacement must reject an unknown wire without mutation");
  740. }
  741. void testSequentialConditionInsertionConsumesFollowingWire()
  742. {
  743. TestProjectStorage storage;
  744. ProjectService project_service(storage);
  745. LogicEditorService service(project_service);
  746. const std::string logic_id = service.ensureDefaultLogic().id;
  747. const std::string rung_id = makeEmptyRung(service, logic_id);
  748. const LogicEditorResult wire = service.appendWire(logic_id, rung_id, 10);
  749. require(wire.succeeded,
  750. "sequential wire replacement must start with a full-width wire");
  751. LogicEditorResult inserted = service.replaceWireColumnWithCondition(
  752. logic_id, rung_id, wire.id, 0, contact(0));
  753. require(inserted.succeeded,
  754. "the first condition must replace the first full-wire cell");
  755. std::string selected_node_id = inserted.id;
  756. for (int address = 1; address < 10; ++address)
  757. {
  758. inserted = service.insertConditionAfter(
  759. logic_id, rung_id, selected_node_id, contact(address));
  760. require(inserted.succeeded,
  761. "continuous condition insertion must consume the following wire cell");
  762. selected_node_id = inserted.id;
  763. const LadderRung *rung = service.findRung(logic_id, rung_id);
  764. std::vector<const LogicNode *> nodes;
  765. collectConditionNodes(*rung->condition, &nodes);
  766. require(rung != nullptr && rung->condition.has_value()
  767. && conditionColumns(*rung->condition) == 10
  768. && nodes.size() == static_cast<std::size_t>(address + 1)
  769. && wireColumns(*rung->condition) == 9 - address,
  770. "each continuous insertion must preserve width while consuming one wire cell");
  771. }
  772. const LadderRung *full = service.findRung(logic_id, rung_id);
  773. require(full != nullptr && full->condition.has_value()
  774. && conditionColumns(*full->condition) == 10
  775. && wireColumns(*full->condition) == 0,
  776. "ten continuous insertions must replace the entire wire without expanding it");
  777. require(service.insertConditionAfter(
  778. logic_id, rung_id, selected_node_id, contact(10))
  779. .error == LogicEditorError::InvalidOperation,
  780. "the eleventh condition must still be rejected after all wire cells are consumed");
  781. require(conditionColumns(*service.findRung(
  782. logic_id, rung_id)->condition) == 10,
  783. "a rejected eleventh insertion must leave the full network unchanged");
  784. require(service.undo().succeeded,
  785. "a failed eleventh insertion must not displace the last successful undo step");
  786. const LadderRung *undone = service.findRung(logic_id, rung_id);
  787. std::vector<const LogicNode *> undone_nodes;
  788. collectConditionNodes(*undone->condition, &undone_nodes);
  789. require(undone_nodes.size() == 9U
  790. && wireColumns(*undone->condition) == 1
  791. && conditionColumns(*undone->condition) == 10,
  792. "undo must restore nine contacts followed by one wire cell");
  793. require(service.redo().succeeded,
  794. "the final wire-consuming insertion must be redoable");
  795. const LadderRung *redone = service.findRung(logic_id, rung_id);
  796. std::vector<const LogicNode *> redone_nodes;
  797. collectConditionNodes(*redone->condition, &redone_nodes);
  798. require(redone_nodes.size() == 10U
  799. && wireColumns(*redone->condition) == 0
  800. && conditionColumns(*redone->condition) == 10,
  801. "redo must restore all ten contacts without wire cells");
  802. }
  803. void testLogicLifecycleAndOrdering()
  804. {
  805. TestProjectStorage storage;
  806. ProjectService project_service(storage);
  807. LogicEditorService service(project_service);
  808. const std::string first_id = service.ensureDefaultLogic().id;
  809. const LogicEditorResult second = service.addLogic("Safety logic");
  810. const LogicEditorResult third = service.addLogic("Alarm logic");
  811. require(second.succeeded && third.succeeded,
  812. "multiple control logic modules must be creatable");
  813. require(service.renameLogic(second.id, "Interlock logic").succeeded,
  814. "control logic modules must be renamable by stable id");
  815. require(service.renameLogic(third.id, "Interlock logic").error
  816. == LogicEditorError::DuplicateName,
  817. "control logic names must remain unique");
  818. require(service.moveLogic(third.id, -1).succeeded
  819. && project_service.project().controlLogics.at(1).id == third.id,
  820. "logic scan order must follow editable vector order");
  821. require(service.setLogicEnabled(second.id, false).succeeded
  822. && !service.findLogic(second.id)->enabled,
  823. "a control logic module must support explicit disable and enable");
  824. require(service.setLogicEnabled(second.id, true).succeeded
  825. && service.findLogic(second.id)->enabled,
  826. "a disabled control logic module must be re-enableable");
  827. require(service.removeLogic(third.id).succeeded,
  828. "a non-final control logic module must be deletable");
  829. require(service.removeLogic(second.id).succeeded,
  830. "logic deletion must preserve the remaining module");
  831. require(service.removeLogic(first_id).error
  832. == LogicEditorError::LastLogicRequired,
  833. "the project must retain at least one control logic module");
  834. }
  835. void testEdgeNodesAndRungComments()
  836. {
  837. TestProjectStorage storage;
  838. ProjectService project_service(storage);
  839. LogicEditorService service(project_service);
  840. const std::string logic_id = service.ensureDefaultLogic().id;
  841. const std::string rung_id = makeEmptyRung(service, logic_id);
  842. const LogicEditorResult rising = service.appendCondition(
  843. logic_id,
  844. rung_id,
  845. EdgeContactNodeConfig{
  846. RegisterAddress{RegisterArea::M, 3}, EdgeMode::Rising});
  847. require(rising.succeeded && rising.id == "edge-1",
  848. "the editor must create rising edge nodes with a stable prefix");
  849. require(service.updateNodeConfig(
  850. logic_id,
  851. rising.id,
  852. EdgeContactNodeConfig{
  853. RegisterAddress{RegisterArea::M, 4}, EdgeMode::Falling})
  854. .succeeded,
  855. "the editor must apply edge mode and M address properties");
  856. require(service.updateRungComment(logic_id, rung_id, "延时启动网络").succeeded,
  857. "the editor must update a network comment by stable rung id");
  858. require(!service.updateRungComment(
  859. logic_id, rung_id, "第一行\n第二行").succeeded,
  860. "the editor must reject multiline network comments");
  861. require(!service.updateRungComment(
  862. logic_id,
  863. rung_id,
  864. std::string(ProjectLimits::kMaximumRungCommentBytes + 1U, 'a'))
  865. .succeeded,
  866. "the editor must reject oversized network comments");
  867. const LadderRung *rung = service.findRung(logic_id, rung_id);
  868. const LogicNode *edge = service.findNode(logic_id, rising.id);
  869. require(rung != nullptr && rung->comment == "延时启动网络"
  870. && edge != nullptr
  871. && std::get<EdgeContactNodeConfig>(edge->config).mode
  872. == EdgeMode::Falling,
  873. "edge and rung comment updates must remain in the model");
  874. }
  875. void testHistoryAndAtomicBatchDelete()
  876. {
  877. TestProjectStorage storage;
  878. ProjectService project_service(storage);
  879. LogicEditorService service(project_service);
  880. const std::string logic_id = service.ensureDefaultLogic().id;
  881. const std::string first_rung_id = makeEmptyRung(service, logic_id);
  882. const LogicEditorResult first = service.appendCondition(
  883. logic_id, first_rung_id, contact(0));
  884. const LogicEditorResult second = service.appendCondition(
  885. logic_id, first_rung_id, contact(1));
  886. const LogicEditorResult second_rung = service.addRung(logic_id);
  887. const LogicEditorResult third = service.appendCondition(
  888. logic_id, second_rung.id, contact(2));
  889. require(first.succeeded && second.succeeded && second_rung.succeeded
  890. && third.succeeded,
  891. "nodes for history testing must be created");
  892. service.clearHistory();
  893. require(!service.removeNodes(logic_id, {first.id, "missing-node"}).succeeded,
  894. "batch node deletion must validate every id before changing the logic");
  895. require(service.findNode(logic_id, first.id) != nullptr
  896. && service.findNode(logic_id, third.id) != nullptr
  897. && !service.canUndo(),
  898. "failed batch node deletion must be atomic and leave history unchanged");
  899. require(service.removeNodes(logic_id, {first.id, third.id}).succeeded,
  900. "valid nodes across multiple rungs must be deleted together");
  901. require(service.findNode(logic_id, first.id) == nullptr
  902. && service.findNode(logic_id, third.id) == nullptr,
  903. "all selected nodes must be removed by one batch operation");
  904. require(service.undo().succeeded
  905. && service.findNode(logic_id, first.id) != nullptr
  906. && service.findNode(logic_id, third.id) != nullptr,
  907. "logic undo must restore a cross-rung batch deletion");
  908. require(service.redo().succeeded
  909. && service.findNode(logic_id, first.id) == nullptr
  910. && service.findNode(logic_id, third.id) == nullptr,
  911. "logic redo must reapply a cross-rung batch deletion");
  912. service.clearHistory();
  913. const ControlLogic *logic = service.findLogic(logic_id);
  914. require(logic != nullptr && service.setLogicEnabled(logic_id, logic->enabled).succeeded
  915. && !service.canUndo(),
  916. "setting an unchanged logic state must not consume history");
  917. service.clearHistory();
  918. for (int index = 1; index <= 101; ++index)
  919. {
  920. require(service.updateRungComment(
  921. logic_id, first_rung_id, "comment-" + std::to_string(index))
  922. .succeeded,
  923. "repeated valid rung edits must succeed");
  924. }
  925. int undo_count = 0;
  926. while (service.undo().succeeded)
  927. {
  928. ++undo_count;
  929. }
  930. require(undo_count == 100,
  931. "logic history must retain exactly the configured 100 most recent steps");
  932. require(service.redo().succeeded,
  933. "logic redo must be available after an undo");
  934. require(service.appendCondition(logic_id, first_rung_id, contact(5)).succeeded,
  935. "a new logic edit must succeed after undo");
  936. require(!service.canRedo(),
  937. "a new logic edit must clear the redo history");
  938. (void)second;
  939. }
  940. void testBatchPasteNodesAndRung()
  941. {
  942. TestProjectStorage storage;
  943. ProjectService project_service(storage);
  944. LogicEditorService service(project_service);
  945. const std::string logic_id = service.ensureDefaultLogic().id;
  946. const std::string rung_id = makeEmptyRung(service, logic_id);
  947. const LogicEditorResult first = service.appendCondition(
  948. logic_id, rung_id, contact(10));
  949. const LogicEditorResult second = service.appendCondition(
  950. logic_id, rung_id, contact(11));
  951. require(first.succeeded && second.succeeded,
  952. "nodes for paste testing must be created");
  953. const LogicNode first_copy = *service.findNode(logic_id, first.id);
  954. const LogicNode second_copy = *service.findNode(logic_id, second.id);
  955. service.clearHistory();
  956. const LogicEditorResult pasted = service.pasteConditionNodes(
  957. logic_id, rung_id, {first_copy, second_copy});
  958. require(pasted.succeeded
  959. && service.findNode(logic_id, pasted.id) != nullptr
  960. && service.findRung(logic_id, rung_id)->condition.has_value()
  961. && conditionNodes(*service.findRung(logic_id, rung_id)->condition) == 4U,
  962. "batch condition paste must append fresh nodes");
  963. require(pasted.id != first.id && pasted.id != second.id
  964. && service.undo().succeeded
  965. && conditionNodes(*service.findRung(logic_id, rung_id)->condition) == 2U,
  966. "batch condition paste must use fresh ids and one undo step");
  967. require(service.areConditionNodesContiguous(
  968. logic_id, rung_id, {first.id, second.id}),
  969. "adjacent nodes in one series must be copyable as a range");
  970. const std::string wire_rung_id = makeEmptyRung(service, logic_id);
  971. const LogicEditorResult wire = service.appendWire(logic_id, wire_rung_id, 3);
  972. service.clearHistory();
  973. LogicConditionPasteTarget wire_target;
  974. wire_target.kind = LogicConditionPasteTargetKind::ReplaceWireColumn;
  975. wire_target.expressionId = wire.id;
  976. wire_target.column = 1;
  977. const LogicEditorResult pasted_on_wire = service.pasteConditionNodes(
  978. logic_id, wire_rung_id, {first_copy, second_copy}, wire_target);
  979. const LadderRung *wire_rung = service.findRung(logic_id, wire_rung_id);
  980. require(pasted_on_wire.succeeded && wire_rung != nullptr
  981. && wire_rung->condition.has_value()
  982. && conditionNodes(*wire_rung->condition) == 2
  983. && conditionColumns(*wire_rung->condition) == 3,
  984. "condition paste must replace the selected wire cell and consume following wire cells");
  985. require(service.undo().succeeded
  986. && wireColumns(*service.findRung(logic_id, wire_rung_id)->condition) == 3,
  987. "wire-targeted paste must be undone as one edit");
  988. const std::string full_rung_id = makeEmptyRung(service, logic_id);
  989. for (int address = 0; address < 9; ++address)
  990. {
  991. require(service.appendCondition(logic_id, full_rung_id, contact(address)).succeeded,
  992. "nine conditions must fit before an atomic paste failure test");
  993. }
  994. service.clearHistory();
  995. require(!service.pasteConditionNodes(
  996. logic_id, full_rung_id, {first_copy, second_copy}).succeeded
  997. && conditionNodes(*service.findRung(logic_id, full_rung_id)->condition) == 9
  998. && !service.canUndo(),
  999. "a multi-node paste that exceeds ten columns must roll back completely");
  1000. const LogicEditorResult third = service.appendCondition(
  1001. logic_id, rung_id, contact(12));
  1002. require(third.succeeded
  1003. && !service.areConditionNodesContiguous(
  1004. logic_id, rung_id, {first.id, third.id}),
  1005. "non-adjacent nodes must not be copied as one condition range");
  1006. const LogicEditorResult output = service.setOutput(
  1007. logic_id,
  1008. rung_id,
  1009. CoilNodeConfig{RegisterAddress{RegisterArea::M, 20}, CoilMode::Set},
  1010. true);
  1011. require(output.succeeded, "the source rung must accept an output before copying");
  1012. const LadderRung source = *service.findRung(logic_id, rung_id);
  1013. service.clearHistory();
  1014. const std::size_t rung_count_before_paste = service.findLogic(logic_id)->rungs.size();
  1015. const LogicEditorResult pasted_rung = service.pasteRung(logic_id, source);
  1016. require(pasted_rung.succeeded
  1017. && service.findLogic(logic_id)->rungs.size()
  1018. == rung_count_before_paste + 1U
  1019. && pasted_rung.id != source.id,
  1020. "whole rung paste must append a new network");
  1021. const LadderRung *copy = service.findRung(logic_id, pasted_rung.id);
  1022. require(copy != nullptr && copy->condition.has_value()
  1023. && copy->condition->id != source.condition->id
  1024. && copy->output.has_value() && source.output.has_value()
  1025. && copy->output->id != source.output->id
  1026. && std::get<CoilNodeConfig>(copy->output->config).address.index() == 20
  1027. && std::get<CoilNodeConfig>(copy->output->config).mode == CoilMode::Set,
  1028. "whole rung paste must regenerate ids and preserve output configuration");
  1029. }
  1030. } // namespace
  1031. int main()
  1032. {
  1033. try
  1034. {
  1035. testEmptyLogicCreatesNetworksOnFirstEdit();
  1036. testAppendingWireMovesToNextNetworkAfterTenColumns();
  1037. testStructuredEditingAndNormalization();
  1038. testRangeParallelInsertion();
  1039. testParallelBranchGridInsertion();
  1040. testStructuredWireEditing();
  1041. testWireCellParallelSelectionUsesExactColumns();
  1042. testWireCellParallelSelectionAcrossAdjacentWires();
  1043. testBatchDeleteAllNodesInParallelBranch();
  1044. testConditionColumnLimit();
  1045. testUnconditionalOutputEditing();
  1046. testColumnTargetedConditionInsertion();
  1047. testWireColumnReplacement();
  1048. testSequentialConditionInsertionConsumesFollowingWire();
  1049. testLogicLifecycleAndOrdering();
  1050. testEdgeNodesAndRungComments();
  1051. testHistoryAndAtomicBatchDelete();
  1052. testBatchPasteNodesAndRung();
  1053. }
  1054. catch (const std::exception &error)
  1055. {
  1056. std::cerr << "logic editor service tests failed: " << error.what() << '\n';
  1057. return 1;
  1058. }
  1059. std::cout << "logic editor service tests passed\n";
  1060. return 0;
  1061. }