综合平台编程器项目的远程存储
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 

487 líneas
22 KiB

  1. #include "domain/project_storage.h"
  2. #include "services/logic_editor_service.h"
  3. #include "services/project_service.h"
  4. #include <iostream>
  5. #include <stdexcept>
  6. namespace {
  7. class TestProjectStorage final : public ProjectStorage
  8. {
  9. public:
  10. ProjectSaveResult save(const Project &, const std::string &) override
  11. {
  12. return {true, ProjectStorageError::None, {}};
  13. }
  14. ProjectLoadResult load(const std::string &) override
  15. {
  16. return {false, {}, ProjectStorageError::FileReadFailed, {}};
  17. }
  18. };
  19. void require(bool condition, const std::string &message)
  20. {
  21. if (!condition)
  22. {
  23. throw std::runtime_error(message);
  24. }
  25. }
  26. ContactNodeConfig contact(int address)
  27. {
  28. return {RegisterAddress{RegisterArea::M, address}, ContactMode::NormallyOpen};
  29. }
  30. void testStructuredEditingAndNormalization()
  31. {
  32. TestProjectStorage storage;
  33. ProjectService project_service(storage);
  34. LogicEditorService service(project_service);
  35. const std::string logic_id = service.ensureDefaultLogic().id;
  36. const std::string rung_id = service.firstRungId(logic_id);
  37. const LogicEditorResult first = service.appendCondition(logic_id, rung_id, contact(0));
  38. const LogicEditorResult second = service.appendCondition(logic_id, rung_id, contact(1));
  39. require(first.succeeded && second.succeeded, "series append must succeed");
  40. const LadderRung *rung = service.findRung(logic_id, rung_id);
  41. require(rung->condition->kind == ConditionExpressionKind::Series
  42. && rung->condition->children.size() == 2U,
  43. "two appended nodes must form a series expression");
  44. const std::string second_expression_id = second.id;
  45. const LogicEditorResult parallel = service.addParallelBranch(
  46. logic_id, rung_id, {second_expression_id}, contact(2));
  47. require(parallel.succeeded, "parallel insertion must succeed");
  48. rung = service.findRung(logic_id, rung_id);
  49. const ConditionExpression *parallel_expression = service.findExpression(
  50. logic_id, rung_id, rung->condition->children.at(1).id);
  51. require(parallel_expression != nullptr
  52. && parallel_expression->kind == ConditionExpressionKind::Parallel,
  53. "selected node must become a parallel expression");
  54. const LogicEditorResult nested_series = service.insertConditionAfter(
  55. logic_id,
  56. rung_id,
  57. parallel.id,
  58. contact(3));
  59. require(nested_series.succeeded, "a parallel branch must accept a series node");
  60. rung = service.findRung(logic_id, rung_id);
  61. require(rung->condition->kind == ConditionExpressionKind::Series,
  62. "root must remain a series expression");
  63. const ConditionExpression &nested_parallel_expression = rung->condition->children.at(1);
  64. require(nested_parallel_expression.kind == ConditionExpressionKind::Parallel
  65. && nested_parallel_expression.children.at(1).kind
  66. == ConditionExpressionKind::Series,
  67. "editor must express A AND (B OR (C AND D))");
  68. require(service.removeNode(logic_id, nested_series.id).succeeded,
  69. "nested series node deletion must succeed");
  70. rung = service.findRung(logic_id, rung_id);
  71. require(rung->condition->children.at(1).kind == ConditionExpressionKind::Parallel
  72. && rung->condition->children.at(1).children.at(1).kind
  73. == ConditionExpressionKind::Node,
  74. "single-child series container must collapse after deletion");
  75. require(service.removeNode(logic_id, parallel.id).succeeded,
  76. "parallel leaf deletion must succeed");
  77. rung = service.findRung(logic_id, rung_id);
  78. require(rung->condition->kind == ConditionExpressionKind::Series
  79. && rung->condition->children.size() == 2U,
  80. "single-child parallel container must collapse after deletion");
  81. require(service.setOutput(
  82. logic_id,
  83. rung_id,
  84. CoilNodeConfig{RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal})
  85. .succeeded,
  86. "output coil must be set");
  87. require(service.findLogic(logic_id)->validate(),
  88. "structured editing result must remain a valid draft");
  89. }
  90. void testRangeParallelInsertion()
  91. {
  92. TestProjectStorage storage;
  93. ProjectService project_service(storage);
  94. LogicEditorService service(project_service);
  95. const std::string logic_id = service.ensureDefaultLogic().id;
  96. const std::string rung_id = service.firstRungId(logic_id);
  97. service.appendCondition(logic_id, rung_id, contact(0));
  98. service.appendCondition(logic_id, rung_id, contact(1));
  99. service.appendCondition(logic_id, rung_id, contact(2));
  100. const LogicEditorResult branch = service.addParallelBranch(
  101. logic_id, rung_id, {"contact-2", "contact-3"}, contact(3));
  102. require(branch.succeeded, "a continuous series range must accept a parallel branch");
  103. const ConditionExpression &root = *service.findRung(logic_id, rung_id)->condition;
  104. require(root.kind == ConditionExpressionKind::Series
  105. && root.children.size() == 2U
  106. && root.children.at(1).kind == ConditionExpressionKind::Parallel
  107. && root.children.at(1).children.front().kind
  108. == ConditionExpressionKind::Series,
  109. "range insertion must express A AND ((B AND C) OR D)");
  110. require(root.validate(), "range insertion must preserve normalized topology");
  111. const LogicEditorResult invalid = service.addParallelBranch(
  112. logic_id, rung_id, {"contact-1", "contact-3"}, contact(4));
  113. require(!invalid.succeeded
  114. && invalid.error == LogicEditorError::InvalidOperation,
  115. "a non-contiguous selection must be rejected");
  116. }
  117. void testStructuredWireEditing()
  118. {
  119. TestProjectStorage storage;
  120. ProjectService project_service(storage);
  121. LogicEditorService service(project_service);
  122. const std::string logic_id = service.ensureDefaultLogic().id;
  123. const std::string rung_id = service.firstRungId(logic_id);
  124. service.appendCondition(logic_id, rung_id, contact(0));
  125. service.appendCondition(logic_id, rung_id, contact(1));
  126. service.appendCondition(logic_id, rung_id, contact(2));
  127. const LogicEditorResult branch = service.addParallelWireBranch(
  128. logic_id, rung_id, {"contact-2", "contact-3"});
  129. require(branch.succeeded && branch.id == "wire-1",
  130. "a continuous range must accept a structured horizontal bypass");
  131. const LadderRung *rung = service.findRung(logic_id, rung_id);
  132. require(rung->condition->kind == ConditionExpressionKind::Series
  133. && rung->condition->children.at(1).kind
  134. == ConditionExpressionKind::Parallel,
  135. "a vertical connection must produce a parallel expression");
  136. const ConditionExpression &wire =
  137. rung->condition->children.at(1).children.at(1);
  138. require(wire.kind == ConditionExpressionKind::Wire
  139. && wire.wire->columnSpan == 2,
  140. "the bypass wire span must match the selected two-column range");
  141. const LogicEditorResult replacement = service.replaceWireWithCondition(
  142. logic_id, rung_id, branch.id, contact(3));
  143. require(replacement.succeeded && replacement.id == "contact-4",
  144. "a selected wire must be replaceable by a configured node type");
  145. const LogicEditorResult extension = service.insertWireAfter(
  146. logic_id, rung_id, replacement.id);
  147. require(extension.succeeded && extension.id == "wire-1",
  148. "a wire id must become reusable after its expression is replaced");
  149. rung = service.findRung(logic_id, rung_id);
  150. const ConditionExpression *extended_branch = service.findExpression(
  151. logic_id, rung_id, rung->condition->children.at(1).children.at(1).id);
  152. require(extended_branch != nullptr
  153. && extended_branch->kind == ConditionExpressionKind::Series
  154. && extended_branch->children.at(1).kind
  155. == ConditionExpressionKind::Wire,
  156. "inserting a horizontal wire after a branch node must preserve structure");
  157. const std::string extended_branch_id = extended_branch->id;
  158. require(!service.addParallelWireBranch(
  159. logic_id, rung_id, {"contact-1", "contact-3"}).succeeded,
  160. "non-contiguous wire connection targets must be rejected");
  161. require(service.removeExpressions(
  162. logic_id, rung_id, {extended_branch_id}).succeeded,
  163. "deleting a selected vertical connection branch must succeed atomically");
  164. rung = service.findRung(logic_id, rung_id);
  165. require(rung->condition->kind == ConditionExpressionKind::Series
  166. && rung->condition->children.size() == 3U,
  167. "removing a bypass branch must normalize back to the original series");
  168. require(service.undo().succeeded
  169. && service.findExpression(logic_id, rung_id, extended_branch_id) != nullptr,
  170. "wire branch deletion must participate in ladder undo history");
  171. }
  172. void testBatchDeleteAllNodesInParallelBranch()
  173. {
  174. TestProjectStorage storage;
  175. ProjectService project_service(storage);
  176. LogicEditorService service(project_service);
  177. const std::string logic_id = service.ensureDefaultLogic().id;
  178. const std::string rung_id = service.firstRungId(logic_id);
  179. const LogicEditorResult first = service.appendCondition(
  180. logic_id, rung_id, contact(0));
  181. const LogicEditorResult second = service.appendCondition(
  182. logic_id, rung_id, contact(1));
  183. const LogicEditorResult third = service.appendCondition(
  184. logic_id, rung_id, contact(2));
  185. const LogicEditorResult fourth = service.appendCondition(
  186. logic_id, rung_id, contact(3));
  187. require(first.succeeded && second.succeeded && third.succeeded
  188. && fourth.succeeded,
  189. "parallel batch deletion setup contacts must be created");
  190. const LogicEditorResult branch = service.addParallelBranch(
  191. logic_id, rung_id,
  192. {second.id, third.id, fourth.id},
  193. contact(10));
  194. require(branch.succeeded,
  195. "parallel batch deletion setup branch must be created");
  196. require(service.removeNodes(
  197. logic_id, {second.id, third.id, fourth.id})
  198. .succeeded,
  199. "deleting every node in a parallel branch as one batch must succeed");
  200. const LadderRung *rung = service.findRung(logic_id, rung_id);
  201. require(rung != nullptr && rung->condition.has_value()
  202. && rung->validate(),
  203. "batch deletion must leave a valid normalized ladder expression");
  204. require(rung->condition->kind == ConditionExpressionKind::Series
  205. && rung->condition->children.size() == 2U
  206. && rung->condition->children.at(0).kind
  207. == ConditionExpressionKind::Node
  208. && rung->condition->children.at(1).kind
  209. == ConditionExpressionKind::Node,
  210. "an empty parallel branch must collapse into the remaining branch");
  211. require(service.findNode(logic_id, second.id) == nullptr
  212. && service.findNode(logic_id, third.id) == nullptr
  213. && service.findNode(logic_id, fourth.id) == nullptr,
  214. "all selected parallel branch nodes must be removed");
  215. require(service.undo().succeeded,
  216. "parallel batch deletion must be undoable");
  217. require(service.findNode(logic_id, second.id) != nullptr
  218. && service.findNode(logic_id, third.id) != nullptr
  219. && service.findNode(logic_id, fourth.id) != nullptr,
  220. "undo must restore every deleted parallel branch node");
  221. }
  222. void testConditionColumnLimit()
  223. {
  224. TestProjectStorage storage;
  225. ProjectService project_service(storage);
  226. LogicEditorService service(project_service);
  227. const std::string logic_id = service.ensureDefaultLogic().id;
  228. const std::string rung_id = service.firstRungId(logic_id);
  229. for (int column = 0; column < ProjectLimits::kMaximumConditionColumns; ++column)
  230. {
  231. require(service.appendCondition(logic_id, rung_id, contact(column)).succeeded,
  232. "the first ten condition columns must be editable");
  233. }
  234. const LogicEditorResult overflow = service.appendCondition(
  235. logic_id, rung_id, contact(ProjectLimits::kMaximumConditionColumns));
  236. require(!overflow.succeeded
  237. && overflow.error == LogicEditorError::InvalidOperation,
  238. "the eleventh condition column must be rejected by the editor service");
  239. const LadderRung *rung = service.findRung(logic_id, rung_id);
  240. require(rung != nullptr && rung->condition.has_value()
  241. && rung->condition->kind == ConditionExpressionKind::Series
  242. && rung->condition->children.size()
  243. == static_cast<std::size_t>(
  244. ProjectLimits::kMaximumConditionColumns),
  245. "a rejected eleventh column must leave the ten-column network unchanged");
  246. }
  247. void testUnconditionalOutputEditing()
  248. {
  249. TestProjectStorage storage;
  250. ProjectService project_service(storage);
  251. LogicEditorService service(project_service);
  252. const std::string logic_id = service.ensureDefaultLogic().id;
  253. const std::string rung_id = service.firstRungId(logic_id);
  254. require(service.setOutput(
  255. logic_id,
  256. rung_id,
  257. CoilNodeConfig{
  258. RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal},
  259. true)
  260. .succeeded,
  261. "the editor must allow a coil before any condition is added");
  262. const LadderRung *rung = service.findRung(logic_id, rung_id);
  263. require(rung != nullptr && !rung->condition.has_value()
  264. && rung->output.has_value() && rung->validateForRunning(),
  265. "an editor-created output-only network must be runnable as unconditional");
  266. }
  267. void testLogicLifecycleAndOrdering()
  268. {
  269. TestProjectStorage storage;
  270. ProjectService project_service(storage);
  271. LogicEditorService service(project_service);
  272. const std::string first_id = service.ensureDefaultLogic().id;
  273. const LogicEditorResult second = service.addLogic("Safety logic");
  274. const LogicEditorResult third = service.addLogic("Alarm logic");
  275. require(second.succeeded && third.succeeded,
  276. "multiple control logic modules must be creatable");
  277. require(service.renameLogic(second.id, "Interlock logic").succeeded,
  278. "control logic modules must be renamable by stable id");
  279. require(service.renameLogic(third.id, "Interlock logic").error
  280. == LogicEditorError::DuplicateName,
  281. "control logic names must remain unique");
  282. require(service.moveLogic(third.id, -1).succeeded
  283. && project_service.project().controlLogics.at(1).id == third.id,
  284. "logic scan order must follow editable vector order");
  285. require(service.setLogicEnabled(second.id, false).succeeded
  286. && !service.findLogic(second.id)->enabled,
  287. "a control logic module must support explicit disable and enable");
  288. require(service.setLogicEnabled(second.id, true).succeeded
  289. && service.findLogic(second.id)->enabled,
  290. "a disabled control logic module must be re-enableable");
  291. require(service.removeLogic(third.id).succeeded,
  292. "a non-final control logic module must be deletable");
  293. require(service.removeLogic(second.id).succeeded,
  294. "logic deletion must preserve the remaining module");
  295. require(service.removeLogic(first_id).error
  296. == LogicEditorError::LastLogicRequired,
  297. "the project must retain at least one control logic module");
  298. }
  299. void testEdgeTimerNodesAndRungComments()
  300. {
  301. TestProjectStorage storage;
  302. ProjectService project_service(storage);
  303. LogicEditorService service(project_service);
  304. const std::string logic_id = service.ensureDefaultLogic().id;
  305. const std::string rung_id = service.firstRungId(logic_id);
  306. const LogicEditorResult rising = service.appendCondition(
  307. logic_id,
  308. rung_id,
  309. EdgeContactNodeConfig{
  310. RegisterAddress{RegisterArea::M, 3}, EdgeMode::Rising});
  311. require(rising.succeeded && rising.id == "edge-1",
  312. "the editor must create rising edge nodes with a stable prefix");
  313. const LogicEditorResult timer = service.addParallelBranch(
  314. logic_id,
  315. rung_id,
  316. {rising.id},
  317. TimerContactNodeConfig{TimerAddress{2}, ContactMode::NormallyOpen});
  318. require(timer.succeeded && timer.id == "timer-contact-1",
  319. "the editor must insert T contacts as conditions");
  320. const LogicEditorResult ton = service.setOutput(
  321. logic_id, rung_id, TonNodeConfig{TimerAddress{2}, 500});
  322. require(ton.succeeded && ton.id == "ton-1",
  323. "the editor must create TON outputs with a stable prefix");
  324. require(service.updateNodeConfig(
  325. logic_id,
  326. rising.id,
  327. EdgeContactNodeConfig{
  328. RegisterAddress{RegisterArea::M, 4}, EdgeMode::Falling})
  329. .succeeded,
  330. "the editor must apply edge mode and M address properties");
  331. require(service.updateNodeConfig(
  332. logic_id,
  333. timer.id,
  334. TimerContactNodeConfig{TimerAddress{4}, ContactMode::NormallyClosed})
  335. .succeeded,
  336. "the editor must apply T contact mode and address properties");
  337. require(service.updateNodeConfig(
  338. logic_id,
  339. ton.id,
  340. TonNodeConfig{TimerAddress{4}, 750})
  341. .succeeded,
  342. "the editor must apply TON preset properties");
  343. require(service.updateRungComment(logic_id, rung_id, "延时启动网络").succeeded,
  344. "the editor must update a network comment by stable rung id");
  345. const LadderRung *rung = service.findRung(logic_id, rung_id);
  346. require(rung != nullptr && rung->comment == "延时启动网络"
  347. && std::get<EdgeContactNodeConfig>(
  348. rung->condition->children.front().node->config).mode
  349. == EdgeMode::Falling
  350. && std::get<TimerContactNodeConfig>(
  351. rung->condition->children.at(1).node->config).address
  352. == TimerAddress{4}
  353. && std::get<TonNodeConfig>(rung->output->config).presetMs == 750,
  354. "edge, T contact, TON and rung comment updates must remain in the model");
  355. }
  356. void testHistoryAndAtomicBatchDelete()
  357. {
  358. TestProjectStorage storage;
  359. ProjectService project_service(storage);
  360. LogicEditorService service(project_service);
  361. const std::string logic_id = service.ensureDefaultLogic().id;
  362. const std::string first_rung_id = service.firstRungId(logic_id);
  363. const LogicEditorResult first = service.appendCondition(
  364. logic_id, first_rung_id, contact(0));
  365. const LogicEditorResult second = service.appendCondition(
  366. logic_id, first_rung_id, contact(1));
  367. const LogicEditorResult second_rung = service.addRung(logic_id);
  368. const LogicEditorResult third = service.appendCondition(
  369. logic_id, second_rung.id, contact(2));
  370. require(first.succeeded && second.succeeded && second_rung.succeeded
  371. && third.succeeded,
  372. "nodes for history testing must be created");
  373. service.clearHistory();
  374. require(!service.removeNodes(logic_id, {first.id, "missing-node"}).succeeded,
  375. "batch node deletion must validate every id before changing the logic");
  376. require(service.findNode(logic_id, first.id) != nullptr
  377. && service.findNode(logic_id, third.id) != nullptr
  378. && !service.canUndo(),
  379. "failed batch node deletion must be atomic and leave history unchanged");
  380. require(service.removeNodes(logic_id, {first.id, third.id}).succeeded,
  381. "valid nodes across multiple rungs must be deleted together");
  382. require(service.findNode(logic_id, first.id) == nullptr
  383. && service.findNode(logic_id, third.id) == nullptr,
  384. "all selected nodes must be removed by one batch operation");
  385. require(service.undo().succeeded
  386. && service.findNode(logic_id, first.id) != nullptr
  387. && service.findNode(logic_id, third.id) != nullptr,
  388. "logic undo must restore a cross-rung batch deletion");
  389. require(service.redo().succeeded
  390. && service.findNode(logic_id, first.id) == nullptr
  391. && service.findNode(logic_id, third.id) == nullptr,
  392. "logic redo must reapply a cross-rung batch deletion");
  393. service.clearHistory();
  394. const ControlLogic *logic = service.findLogic(logic_id);
  395. require(logic != nullptr && service.setLogicEnabled(logic_id, logic->enabled).succeeded
  396. && !service.canUndo(),
  397. "setting an unchanged logic state must not consume history");
  398. service.clearHistory();
  399. for (int index = 1; index <= 101; ++index)
  400. {
  401. require(service.updateRungComment(
  402. logic_id, first_rung_id, "comment-" + std::to_string(index))
  403. .succeeded,
  404. "repeated valid rung edits must succeed");
  405. }
  406. int undo_count = 0;
  407. while (service.undo().succeeded)
  408. {
  409. ++undo_count;
  410. }
  411. require(undo_count == 100,
  412. "logic history must retain exactly the configured 100 most recent steps");
  413. require(service.redo().succeeded,
  414. "logic redo must be available after an undo");
  415. require(service.appendCondition(logic_id, first_rung_id, contact(5)).succeeded,
  416. "a new logic edit must succeed after undo");
  417. require(!service.canRedo(),
  418. "a new logic edit must clear the redo history");
  419. (void)second;
  420. }
  421. } // namespace
  422. int main()
  423. {
  424. try
  425. {
  426. testStructuredEditingAndNormalization();
  427. testRangeParallelInsertion();
  428. testStructuredWireEditing();
  429. testBatchDeleteAllNodesInParallelBranch();
  430. testConditionColumnLimit();
  431. testUnconditionalOutputEditing();
  432. testLogicLifecycleAndOrdering();
  433. testEdgeTimerNodesAndRungComments();
  434. testHistoryAndAtomicBatchDelete();
  435. }
  436. catch (const std::exception &error)
  437. {
  438. std::cerr << "logic editor service tests failed: " << error.what() << '\n';
  439. return 1;
  440. }
  441. std::cout << "logic editor service tests passed\n";
  442. return 0;
  443. }