综合平台编程器项目的远程存储
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

687 строки
27 KiB

  1. #include "domain/hmi_model.h"
  2. #include "domain/register_repository.h"
  3. #include "domain/virtual_register_repository.h"
  4. #include "services/hmi_runtime_service.h"
  5. #include "services/offline_simulation_service.h"
  6. #include "services/software_logic_executor.h"
  7. #include "support/test_support.h"
  8. #include <QCoreApplication>
  9. #include <array>
  10. #include <iostream>
  11. #include <stdexcept>
  12. #include <string>
  13. #include <vector>
  14. namespace {
  15. using TestSupport::require;
  16. LogicNode contact(const std::string &id, int address,
  17. ContactMode mode = ContactMode::NormallyOpen)
  18. {
  19. return {id, ContactNodeConfig{RegisterAddress{RegisterArea::M, address}, mode}, true};
  20. }
  21. LogicNode edgeContact(const std::string &id, int address, EdgeMode mode)
  22. {
  23. return {id,
  24. EdgeContactNodeConfig{
  25. RegisterAddress{RegisterArea::M, address}, mode},
  26. true};
  27. }
  28. WordOperand constantOperand(std::int16_t value)
  29. {
  30. return {
  31. WordOperandKind::Constant,
  32. RegisterAddress{RegisterArea::D, 0},
  33. value};
  34. }
  35. WordOperand registerOperand(int address)
  36. {
  37. return {
  38. WordOperandKind::Register,
  39. RegisterAddress{RegisterArea::D, address},
  40. 0};
  41. }
  42. LogicNode move(
  43. const std::string &id,
  44. WordOperand source,
  45. int destination)
  46. {
  47. return {
  48. id,
  49. MoveNodeConfig{
  50. source,
  51. RegisterAddress{RegisterArea::D, destination}},
  52. true};
  53. }
  54. LogicNode arithmetic(
  55. const std::string &id,
  56. ArithmeticOperation operation,
  57. WordOperand left,
  58. WordOperand right,
  59. int destination)
  60. {
  61. return {
  62. id,
  63. ArithmeticNodeConfig{
  64. operation,
  65. left,
  66. right,
  67. RegisterAddress{RegisterArea::D, destination}},
  68. true};
  69. }
  70. LogicNode comparison(const std::string &id, int address,
  71. ComparisonOperator operation, std::int16_t value)
  72. {
  73. return {id,
  74. CompareNodeConfig{RegisterAddress{RegisterArea::D, address}, operation, value},
  75. true};
  76. }
  77. LogicNode coil(const std::string &id, int address,
  78. CoilMode mode = CoilMode::Normal)
  79. {
  80. return {id, CoilNodeConfig{RegisterAddress{RegisterArea::M, address}, mode}, true};
  81. }
  82. LadderRung rung(const std::string &id,
  83. const std::vector<std::vector<LogicNode>> &stages,
  84. const LogicNode &output)
  85. {
  86. LadderRung result;
  87. result.id = id;
  88. result.name = id;
  89. std::vector<ConditionExpression> series_children;
  90. for (std::size_t index = 0; index < stages.size(); ++index)
  91. {
  92. std::vector<ConditionExpression> parallel_children;
  93. for (const LogicNode &node : stages[index])
  94. {
  95. parallel_children.push_back(ConditionExpression::fromNode(node));
  96. }
  97. if (parallel_children.size() == 1U)
  98. {
  99. series_children.push_back(std::move(parallel_children.front()));
  100. }
  101. else
  102. {
  103. ConditionExpression parallel;
  104. parallel.id = id + "-parallel-" + std::to_string(index);
  105. parallel.kind = ConditionExpressionKind::Parallel;
  106. parallel.children = std::move(parallel_children);
  107. series_children.push_back(std::move(parallel));
  108. }
  109. }
  110. if (series_children.size() == 1U)
  111. {
  112. result.condition = std::move(series_children.front());
  113. }
  114. else
  115. {
  116. ConditionExpression series;
  117. series.id = id + "-series";
  118. series.kind = ConditionExpressionKind::Series;
  119. series.children = std::move(series_children);
  120. result.condition = std::move(series);
  121. }
  122. result.output = output;
  123. return result;
  124. }
  125. ControlLogic logic(const std::vector<LadderRung> &rungs)
  126. {
  127. return {"logic-1", "logic-1", rungs, true};
  128. }
  129. bool readBit(RegisterRepository &repository, int address)
  130. {
  131. const BitReadResult result = repository.readBit(
  132. RegisterAddress{RegisterArea::M, address});
  133. require(result.succeeded, "test register read must succeed");
  134. return result.value;
  135. }
  136. void writeBit(RegisterRepository &repository, int address, bool value)
  137. {
  138. require(repository.writeBit(RegisterAddress{RegisterArea::M, address}, value).succeeded,
  139. "test bit write must succeed");
  140. }
  141. void writeWord(RegisterRepository &repository, int address, std::int16_t value)
  142. {
  143. require(repository.writeWord(RegisterAddress{RegisterArea::D, address}, value).succeeded,
  144. "test word write must succeed");
  145. }
  146. std::int16_t readWord(RegisterRepository &repository, int address)
  147. {
  148. const WordReadResult result = repository.readWord(
  149. RegisterAddress{RegisterArea::D, address});
  150. require(result.succeeded, "test word read must succeed");
  151. return result.value;
  152. }
  153. void testNestedSeriesParallelExpression()
  154. {
  155. VirtualRegisterRepository repository;
  156. SoftwareLogicExecutor executor;
  157. ConditionExpression nested_series;
  158. nested_series.id = "nested-series";
  159. nested_series.kind = ConditionExpressionKind::Series;
  160. nested_series.children = {
  161. ConditionExpression::fromNode(contact("b", 1)),
  162. ConditionExpression::fromNode(contact("c", 2))};
  163. ConditionExpression root;
  164. root.id = "root-parallel";
  165. root.kind = ConditionExpressionKind::Parallel;
  166. root.children = {
  167. ConditionExpression::fromNode(contact("a", 0)),
  168. nested_series};
  169. LadderRung nested_rung;
  170. nested_rung.id = "nested-rung";
  171. nested_rung.name = "nested-rung";
  172. nested_rung.condition = root;
  173. nested_rung.output = coil("nested-output", 10);
  174. const ControlLogic program = logic({nested_rung});
  175. writeBit(repository, 1, true);
  176. writeBit(repository, 2, true);
  177. LogicTraceSnapshot trace;
  178. require(executor.executeScan({program}, repository, &trace).succeeded,
  179. "nested expression scan must succeed");
  180. require(readBit(repository, 10), "B AND C branch must energize A OR (B AND C)");
  181. require(trace.expressionValues.at("nested-series")
  182. && trace.expressionValues.at("root-parallel")
  183. && trace.rungValues.at("nested-rung"),
  184. "scan trace must expose active nested expression and rung values");
  185. writeBit(repository, 2, false);
  186. require(executor.executeScan({program}, repository, &trace).succeeded,
  187. "nested false scan must succeed");
  188. require(!readBit(repository, 10), "incomplete B AND C branch must be false");
  189. writeBit(repository, 0, true);
  190. require(executor.executeScan({program}, repository, &trace).succeeded,
  191. "alternate branch scan must succeed");
  192. require(readBit(repository, 10), "A branch must independently energize output");
  193. }
  194. void testUnconditionalCoil()
  195. {
  196. VirtualRegisterRepository repository;
  197. SoftwareLogicExecutor executor;
  198. LadderRung unconditional;
  199. unconditional.id = "unconditional-rung";
  200. unconditional.name = "unconditional-rung";
  201. unconditional.output = coil("unconditional-coil", 10);
  202. const ControlLogic program = logic({unconditional});
  203. LogicTraceSnapshot trace;
  204. require(executor.validate({program}).succeeded,
  205. "an output-only network must pass runtime validation");
  206. require(executor.executeScan({program}, repository, &trace).succeeded,
  207. "an output-only network scan must succeed");
  208. require(readBit(repository, 10),
  209. "an output-only network must energize its coil as a constant-true rung");
  210. require(trace.rungValues.at("unconditional-rung")
  211. && trace.nodePowerValues.at("unconditional-coil"),
  212. "an unconditional rung must report energized power flow");
  213. }
  214. void testWirePassThroughAndPowerTrace()
  215. {
  216. VirtualRegisterRepository repository;
  217. SoftwareLogicExecutor executor;
  218. ConditionExpression root;
  219. root.id = "wire-series";
  220. root.kind = ConditionExpressionKind::Series;
  221. root.children = {
  222. ConditionExpression::fromNode(contact("wire-input", 0)),
  223. ConditionExpression::fromWire("wire-segment", 2),
  224. ConditionExpression::fromNode(contact("wire-output", 1))};
  225. LadderRung wired_rung;
  226. wired_rung.id = "wired-rung";
  227. wired_rung.name = "wired-rung";
  228. wired_rung.condition = root;
  229. wired_rung.output = coil("wired-coil", 10);
  230. const ControlLogic program = logic({wired_rung});
  231. writeBit(repository, 1, true);
  232. LogicTraceSnapshot trace;
  233. require(executor.executeScan({program}, repository, &trace).succeeded,
  234. "wire expression scan must succeed");
  235. require(!readBit(repository, 10),
  236. "a horizontal wire must not bypass a false upstream series contact");
  237. require(trace.expressionValues.at("wire-segment")
  238. && !trace.expressionInputValues.at("wire-segment")
  239. && !trace.expressionPowerValues.at("wire-segment"),
  240. "a wire must remain logically true without showing false upstream power");
  241. writeBit(repository, 0, true);
  242. require(executor.executeScan({program}, repository, &trace).succeeded,
  243. "powered wire expression scan must succeed");
  244. require(readBit(repository, 10)
  245. && trace.expressionInputValues.at("wire-segment")
  246. && trace.expressionPowerValues.at("wire-segment"),
  247. "a powered horizontal wire must pass current to the downstream contact");
  248. }
  249. void testSeriesParallelContactsAndSequentialVisibility()
  250. {
  251. VirtualRegisterRepository repository;
  252. SoftwareLogicExecutor executor;
  253. const ControlLogic program = logic({
  254. rung("rung-1",
  255. {{contact("start", 0), contact("alternate", 1)},
  256. {contact("stop", 2, ContactMode::NormallyClosed)}},
  257. coil("run", 3)),
  258. rung("rung-2", {{contact("run-feedback", 3)}}, coil("downstream", 4))});
  259. writeBit(repository, 1, true);
  260. require(executor.executeScan({program}, repository).succeeded,
  261. "parallel and series scan must succeed");
  262. require(readBit(repository, 3), "parallel OR and series AND must energize output");
  263. require(readBit(repository, 4),
  264. "a later rung must see an earlier rung write in the same scan");
  265. writeBit(repository, 2, true);
  266. require(executor.executeScan({program}, repository).succeeded,
  267. "normally closed scan must succeed");
  268. require(!readBit(repository, 3), "normally closed stop contact must open the rung");
  269. require(!readBit(repository, 4), "downstream normal coil must follow the new value");
  270. }
  271. void testAllComparisons()
  272. {
  273. const std::array<ComparisonOperator, 6> operations{
  274. ComparisonOperator::Equal, ComparisonOperator::NotEqual,
  275. ComparisonOperator::LessThan, ComparisonOperator::LessThanOrEqual,
  276. ComparisonOperator::GreaterThan, ComparisonOperator::GreaterThanOrEqual};
  277. const std::array<std::int16_t, 6> actual_values{10, 9, 9, 10, 11, 10};
  278. for (std::size_t index = 0; index < operations.size(); ++index)
  279. {
  280. VirtualRegisterRepository repository;
  281. SoftwareLogicExecutor executor;
  282. writeWord(repository, 0, actual_values[index]);
  283. const ControlLogic program = logic({
  284. rung("compare-rung", {{comparison("compare", 0, operations[index], 10)},
  285. }, coil("result", 10))});
  286. require(executor.executeScan({program}, repository).succeeded,
  287. "comparison scan must succeed");
  288. require(readBit(repository, 10), "comparison operator must evaluate true");
  289. }
  290. }
  291. void testSetResetAndDisabledLogic()
  292. {
  293. VirtualRegisterRepository repository;
  294. SoftwareLogicExecutor executor;
  295. ControlLogic program = logic({
  296. rung("set-rung", {{contact("set-input", 0)}}, coil("set-output", 5, CoilMode::Set)),
  297. rung("reset-rung", {{contact("reset-input", 1)}},
  298. coil("reset-output", 6, CoilMode::Reset))});
  299. writeBit(repository, 0, true);
  300. require(executor.executeScan({program}, repository).succeeded, "set scan must succeed");
  301. require(readBit(repository, 5), "set coil must latch true");
  302. writeBit(repository, 0, false);
  303. require(executor.executeScan({program}, repository).succeeded,
  304. "inactive set scan must succeed");
  305. require(readBit(repository, 5), "inactive set coil must retain its value");
  306. writeBit(repository, 1, true);
  307. writeBit(repository, 6, true);
  308. require(executor.executeScan({program}, repository).succeeded,
  309. "reset scan must succeed");
  310. require(!readBit(repository, 6), "reset coil must write false");
  311. program.enabled = false;
  312. writeBit(repository, 5, true);
  313. require(executor.executeScan({program}, repository).succeeded,
  314. "disabled logic scan must be ignored successfully");
  315. require(readBit(repository, 5), "disabled logic must not change outputs");
  316. }
  317. void testMultipleLogicScanOrderAndTraceIsolation()
  318. {
  319. VirtualRegisterRepository repository;
  320. SoftwareLogicExecutor executor;
  321. ControlLogic first = logic({
  322. rung("rung-1", {{contact("input", 0)}}, coil("output", 1))});
  323. first.id = "logic-first";
  324. first.name = "First";
  325. ControlLogic second = logic({
  326. rung("rung-1", {{contact("input", 1)}}, coil("output", 2))});
  327. second.id = "logic-second";
  328. second.name = "Second";
  329. writeBit(repository, 0, true);
  330. LogicTraceSnapshot trace;
  331. require(executor.executeScan({first, second}, repository, &trace).succeeded,
  332. "all enabled logic modules must execute in project order");
  333. require(readBit(repository, 1) && readBit(repository, 2),
  334. "a later logic module must observe an earlier module write in one scan");
  335. require(trace.logicValues.size() == 2U
  336. && trace.forLogic(first.id).rungValues.at("rung-1")
  337. && trace.forLogic(second.id).rungValues.at("rung-1"),
  338. "runtime traces must be partitioned by logic id when node ids repeat");
  339. ControlLogic disabled_draft;
  340. disabled_draft.id = "logic-draft";
  341. disabled_draft.name = "Draft";
  342. disabled_draft.enabled = false;
  343. disabled_draft.rungs.push_back(
  344. {"rung-1", "Draft", {}, std::nullopt, std::nullopt});
  345. require(executor.validate({first, disabled_draft}).succeeded,
  346. "a disabled incomplete logic module must not block offline execution");
  347. }
  348. void testEdgeContactsAreOneScanPulsesAndAreLogicScoped()
  349. {
  350. VirtualRegisterRepository repository;
  351. SoftwareLogicExecutor executor;
  352. const ControlLogic program = logic({
  353. rung("rising-rung", {{edgeContact("rising", 0, EdgeMode::Rising)}},
  354. coil("rising-output", 10)),
  355. rung("falling-rung", {{edgeContact("falling", 1, EdgeMode::Falling)}},
  356. coil("falling-output", 11))});
  357. writeBit(repository, 0, true);
  358. require(executor.executeScan({program}, repository).succeeded,
  359. "the first high scan must evaluate rising edge input");
  360. require(readBit(repository, 10), "rising edge must pulse on OFF to ON");
  361. require(!readBit(repository, 11), "falling edge must stay off without ON to OFF");
  362. require(executor.executeScan({program}, repository).succeeded,
  363. "a repeated high scan must succeed");
  364. require(!readBit(repository, 10), "rising edge must clear after one scan");
  365. writeBit(repository, 1, true);
  366. require(executor.executeScan({program}, repository).succeeded,
  367. "the falling edge input must establish its previous high state");
  368. writeBit(repository, 1, false);
  369. require(executor.executeScan({program}, repository).succeeded,
  370. "the first low scan after high must succeed");
  371. require(readBit(repository, 11), "falling edge must pulse on ON to OFF");
  372. require(executor.executeScan({program}, repository).succeeded,
  373. "a repeated low scan must succeed");
  374. require(!readBit(repository, 11), "falling edge must clear after one scan");
  375. ControlLogic first = logic({
  376. rung("shared-rung-first", {{edgeContact("shared-edge", 2, EdgeMode::Rising)}},
  377. coil("shared-output-first", 20))});
  378. first.id = "logic-first";
  379. first.name = "First";
  380. ControlLogic second = logic({
  381. rung("shared-rung-second", {{edgeContact("shared-edge", 3, EdgeMode::Rising)}},
  382. coil("shared-output-second", 21))});
  383. second.id = "logic-second";
  384. second.name = "Second";
  385. writeBit(repository, 2, true);
  386. require(executor.executeScan({first, second}, repository).succeeded,
  387. "logic-scoped edge history must support duplicate node ids");
  388. writeBit(repository, 3, true);
  389. require(executor.executeScan({first, second}, repository).succeeded,
  390. "each duplicate edge node must update its own history");
  391. require(!readBit(repository, 20) && readBit(repository, 21),
  392. "duplicate node ids in different logic modules must not share edge history");
  393. executor.resetRuntime();
  394. require(executor.executeScan({first, second}, repository).succeeded,
  395. "resetting runtime must clear edge history");
  396. require(readBit(repository, 20) && readBit(repository, 21),
  397. "a restarted runtime must treat current ON inputs as fresh rising edges");
  398. }
  399. void testMoveAndSaturatingArithmetic()
  400. {
  401. VirtualRegisterRepository repository;
  402. SoftwareLogicExecutor executor;
  403. const ControlLogic program = logic({
  404. rung(
  405. "move-rung",
  406. {{contact("execute", 0)}},
  407. move("move", constantOperand(7), 10)),
  408. rung(
  409. "add-rung",
  410. {{contact("execute-add", 0)}},
  411. arithmetic(
  412. "add",
  413. ArithmeticOperation::Add,
  414. registerOperand(10),
  415. constantOperand(32767),
  416. 11)),
  417. rung(
  418. "sub-rung",
  419. {{contact("execute-sub", 0)}},
  420. arithmetic(
  421. "sub",
  422. ArithmeticOperation::Subtract,
  423. constantOperand(-32768),
  424. constantOperand(1),
  425. 12))});
  426. LogicTraceSnapshot trace;
  427. require(executor.executeScan({program}, repository, &trace).succeeded,
  428. "inactive data instructions must scan successfully");
  429. require(readWord(repository, 10) == 0,
  430. "MOVE must not write while its rung is false");
  431. writeBit(repository, 0, true);
  432. require(executor.executeScan({program}, repository, &trace).succeeded,
  433. "active data instructions must scan successfully");
  434. require(readWord(repository, 10) == 7,
  435. "MOVE must copy a constant into the destination D register");
  436. require(readWord(repository, 11) == 32767
  437. && trace.wordValues.at("add").overflow,
  438. "ADD must saturate positive overflow and expose the overflow trace");
  439. require(readWord(repository, 12) == -32768
  440. && trace.wordValues.at("sub").overflow,
  441. "SUB must saturate negative overflow and expose the overflow trace");
  442. const ControlLogic repeated_add = logic({
  443. rung(
  444. "repeated-add-rung",
  445. {{contact("repeated-add-input", 1)}},
  446. arithmetic(
  447. "repeated-add",
  448. ArithmeticOperation::Add,
  449. registerOperand(20),
  450. constantOperand(1),
  451. 20))});
  452. writeBit(repository, 1, true);
  453. require(executor.executeScan({repeated_add}, repository).succeeded
  454. && executor.executeScan({repeated_add}, repository).succeeded,
  455. "ADD must execute on every scan while its rung remains true");
  456. require(readWord(repository, 20) == 2,
  457. "ADD with the same source and destination must accumulate by scan");
  458. }
  459. void testSetResetPairOnSameAddress()
  460. {
  461. VirtualRegisterRepository repository;
  462. SoftwareLogicExecutor executor;
  463. const ControlLogic program = logic({
  464. rung("low-level-rung",
  465. {{comparison("low-level", 0,
  466. ComparisonOperator::LessThanOrEqual, 30)}},
  467. coil("pump-set", 20, CoilMode::Set)),
  468. rung("high-level-rung",
  469. {{comparison("high-level", 0,
  470. ComparisonOperator::GreaterThanOrEqual, 80)}},
  471. coil("pump-reset", 20, CoilMode::Reset))});
  472. writeWord(repository, 0, 20);
  473. require(executor.executeScan({program}, repository).succeeded,
  474. "set/reset pair scan must succeed at the low limit");
  475. require(readBit(repository, 20), "low level must latch the pump on");
  476. writeWord(repository, 0, 50);
  477. require(executor.executeScan({program}, repository).succeeded,
  478. "set/reset pair scan must succeed inside the deadband");
  479. require(readBit(repository, 20), "deadband must retain the latched on state");
  480. writeWord(repository, 0, 90);
  481. require(executor.executeScan({program}, repository).succeeded,
  482. "set/reset pair scan must succeed at the high limit");
  483. require(!readBit(repository, 20), "high level must reset the pump");
  484. writeWord(repository, 0, 50);
  485. require(executor.executeScan({program}, repository).succeeded,
  486. "set/reset pair rescan must succeed inside the deadband");
  487. require(!readBit(repository, 20), "deadband must retain the reset state");
  488. }
  489. void testConflictingCoilsAreRejected()
  490. {
  491. SoftwareLogicExecutor executor;
  492. const ControlLogic normal_and_set = logic({
  493. rung("normal-rung", {{contact("normal-input", 0)}}, coil("normal", 8)),
  494. rung("set-rung", {{contact("set-input", 1)}}, coil("set", 8, CoilMode::Set))});
  495. const LogicScanResult set_result = executor.validate({normal_and_set});
  496. require(!set_result.succeeded
  497. && set_result.error == LogicScanError::ConflictingOutput,
  498. "normal and set coils for one address must be rejected");
  499. require(set_result.rungId == "set-rung" && set_result.nodeId == "set",
  500. "conflict error must identify the offending rung and node");
  501. const ControlLogic reset_and_normal = logic({
  502. rung("reset-rung", {{contact("reset-input", 0)}},
  503. coil("reset", 9, CoilMode::Reset)),
  504. rung("normal-rung", {{contact("normal-input", 1)}}, coil("normal", 9))});
  505. const LogicScanResult reset_result = executor.validate({reset_and_normal});
  506. require(!reset_result.succeeded
  507. && reset_result.error == LogicScanError::ConflictingOutput,
  508. "reset and normal coils for one address must be rejected");
  509. }
  510. void testHmiSimulationClosedLoop()
  511. {
  512. VirtualRegisterRepository repository;
  513. HmiRuntimeService hmi(repository);
  514. SoftwareLogicExecutor executor;
  515. HmiControl start;
  516. start.id = "start-button";
  517. start.type = HmiControlType::Button;
  518. start.bounds = {0, 0, 80, 30};
  519. start.text = "start";
  520. start.binding = RegisterAddress{RegisterArea::M, 0};
  521. start.buttonOperation = HmiButtonOperation::MomentaryOn;
  522. HmiControl indicator;
  523. indicator.id = "run-indicator";
  524. indicator.type = HmiControlType::Indicator;
  525. indicator.bounds = {0, 40, 80, 30};
  526. indicator.text = "run";
  527. indicator.binding = RegisterAddress{RegisterArea::M, 2};
  528. const ControlLogic program = logic({
  529. rung("hold-rung",
  530. {{contact("stop", 1, ContactMode::NormallyClosed)},
  531. {contact("start", 0), contact("feedback", 2)}},
  532. coil("run", 2))});
  533. require(hmi.operateButton(start, HmiButtonEvent::Pressed).succeeded,
  534. "HMI start button press must write virtual M");
  535. require(executor.executeScan({program}, repository).succeeded,
  536. "closed-loop scan must succeed");
  537. require(hmi.readControl(indicator).bit_value,
  538. "HMI indicator must observe the logic output");
  539. require(hmi.operateButton(start, HmiButtonEvent::Released).succeeded,
  540. "HMI start button release must write virtual M");
  541. require(executor.executeScan({program}, repository).succeeded, "holding scan must succeed");
  542. require(hmi.readControl(indicator).bit_value,
  543. "feedback contact must hold the output after start turns off");
  544. writeBit(repository, 1, true);
  545. require(executor.executeScan({program}, repository).succeeded, "stop scan must succeed");
  546. require(!hmi.readControl(indicator).bit_value,
  547. "stop contact must clear the HMI run indication");
  548. }
  549. void testSimulationLifecycleSnapshotAndFault()
  550. {
  551. VirtualRegisterRepository repository;
  552. OfflineSimulationService simulation(repository);
  553. ControlLogic program = logic({
  554. rung("snapshot-rung", {{contact("input", 0)}}, coil("output", 1))});
  555. writeBit(repository, 0, true);
  556. require(simulation.start({program}).succeeded, "simulation must start");
  557. require(simulation.state() == SimulationState::Running, "simulation must report running");
  558. require(!readBit(repository, 0), "starting a session must clear virtual registers");
  559. require(simulation.scanIntervalMs() == 50, "default scan interval must be 50 ms");
  560. writeBit(repository, 0, true);
  561. program.rungs.front().output = coil("changed-output", 9);
  562. require(simulation.executeOnce().succeeded, "manual lifecycle scan must succeed");
  563. require(readBit(repository, 1), "simulation must use its start-time snapshot");
  564. require(simulation.successfulScanCount() == 1, "successful scans must be counted");
  565. simulation.stop();
  566. require(simulation.state() == SimulationState::Stopped, "simulation must stop");
  567. require(simulation.start({program}).succeeded, "stopped simulation must support restart");
  568. require(!readBit(repository, 1), "new session must clear prior output values");
  569. simulation.stop();
  570. }
  571. class FailingVirtualRegisterRepository final : public VirtualRegisterRepository
  572. {
  573. public:
  574. BitReadResult readBit(const RegisterAddress &) const override
  575. {
  576. return {false, false, RegisterError::Unavailable};
  577. }
  578. };
  579. void testRepositoryFailureEntersFaultState()
  580. {
  581. FailingVirtualRegisterRepository repository;
  582. OfflineSimulationService simulation(repository);
  583. const ControlLogic program = logic({
  584. rung("fault-rung", {{contact("fault-input", 0)}}, coil("fault-output", 1))});
  585. require(simulation.start({program}).succeeded,
  586. "valid logic must pass simulation startup");
  587. const LogicScanResult result = simulation.executeOnce();
  588. require(!result.succeeded && result.error == LogicScanError::RegisterReadFailed,
  589. "repository read failure must fail the scan");
  590. require(simulation.state() == SimulationState::Faulted,
  591. "repository failure must enter the fault state");
  592. require(simulation.lastError().logicId == "logic-1"
  593. && simulation.lastError().rungId == "fault-rung"
  594. && simulation.lastError().nodeId == "fault-input",
  595. "fault feedback must retain logic, rung and node context");
  596. }
  597. } // namespace
  598. int main(int argc, char *argv[])
  599. {
  600. QCoreApplication application(argc, argv);
  601. try
  602. {
  603. testSeriesParallelContactsAndSequentialVisibility();
  604. testNestedSeriesParallelExpression();
  605. testUnconditionalCoil();
  606. testWirePassThroughAndPowerTrace();
  607. testAllComparisons();
  608. testSetResetAndDisabledLogic();
  609. testMultipleLogicScanOrderAndTraceIsolation();
  610. testEdgeContactsAreOneScanPulsesAndAreLogicScoped();
  611. testMoveAndSaturatingArithmetic();
  612. testSetResetPairOnSameAddress();
  613. testConflictingCoilsAreRejected();
  614. testHmiSimulationClosedLoop();
  615. testSimulationLifecycleSnapshotAndFault();
  616. testRepositoryFailureEntersFaultState();
  617. }
  618. catch (const std::exception &error)
  619. {
  620. std::cerr << "offline simulation service tests failed: " << error.what() << '\n';
  621. return 1;
  622. }
  623. std::cout << "offline simulation service tests passed\n";
  624. return 0;
  625. }