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

598 rivejä
24 KiB

  1. #include "services/runtime_mode_service.h"
  2. #include "services/user_runtime_plc_recovery_policy.h"
  3. #include "services/logic_editor_service.h"
  4. #include "services/offline_simulation_service.h"
  5. #include "services/project_service.h"
  6. #include "domain/active_register_repository.h"
  7. #include "domain/project_storage.h"
  8. #include "domain/register_repository.h"
  9. #include "domain/virtual_register_repository.h"
  10. #include "infrastructure/plc_register_repository.h"
  11. #include "support/test_support.h"
  12. #include <functional>
  13. #include <iostream>
  14. #include <stdexcept>
  15. #include <string>
  16. #include <utility>
  17. namespace {
  18. using TestProjectStorage = TestSupport::InMemoryProjectStorage;
  19. class ReadyPlcGateway final : public PlcCommunicationGateway
  20. {
  21. public:
  22. PlcCommunicationResult connectDevice(const PlcSerialConfiguration &) override
  23. {
  24. connection_state = PlcConnectionState::Connected;
  25. if (state_changed)
  26. {
  27. state_changed();
  28. }
  29. return {true, {}};
  30. }
  31. void disconnectDevice() override
  32. {
  33. connection_state = PlcConnectionState::Disconnected;
  34. initial_read = false;
  35. }
  36. PlcCommunicationResult setPollAddresses(
  37. const std::vector<RegisterAddress> &addresses) override
  38. {
  39. return setPollAddresses(addresses, {});
  40. }
  41. PlcCommunicationResult setPollAddresses(
  42. const std::vector<RegisterAddress> &addresses,
  43. const std::vector<RegisterWordRange> &ranges) override
  44. {
  45. if (reject_poll_configuration)
  46. {
  47. return {false, "poll configuration rejected"};
  48. }
  49. poll_addresses = addresses;
  50. poll_ranges = ranges;
  51. return {true, {}};
  52. }
  53. PlcConnectionState state() const override { return connection_state; }
  54. bool initialReadCompleted() const override { return initial_read; }
  55. PlcCommunicationError lastErrorType() const override
  56. {
  57. return PlcCommunicationError::None;
  58. }
  59. const std::string &lastError() const override { return last_error; }
  60. void setCallbacks(
  61. std::function<void()> state_callback,
  62. std::function<void(bool)> initial_callback,
  63. std::function<void()> cache_callback,
  64. std::function<void()> poll_cycle_callback,
  65. std::function<void(const std::string &)> error_callback) override
  66. {
  67. state_changed = std::move(state_callback);
  68. initial_read_changed = std::move(initial_callback);
  69. cache_updated = std::move(cache_callback);
  70. poll_cycle_completed = std::move(poll_cycle_callback);
  71. error_reported = std::move(error_callback);
  72. }
  73. void completeInitialRead()
  74. {
  75. initial_read = true;
  76. if (initial_read_changed)
  77. {
  78. initial_read_changed(true);
  79. }
  80. }
  81. void completePollCycle()
  82. {
  83. if (poll_cycle_completed)
  84. {
  85. poll_cycle_completed();
  86. }
  87. }
  88. void reportFault()
  89. {
  90. connection_state = PlcConnectionState::Faulted;
  91. initial_read = false;
  92. if (state_changed)
  93. {
  94. state_changed();
  95. }
  96. }
  97. const std::vector<RegisterAddress> &pollAddresses() const
  98. {
  99. return poll_addresses;
  100. }
  101. const std::vector<RegisterWordRange> &pollRanges() const
  102. {
  103. return poll_ranges;
  104. }
  105. bool reject_poll_configuration = false;
  106. private:
  107. PlcConnectionState connection_state = PlcConnectionState::Disconnected;
  108. bool initial_read = false;
  109. std::string last_error;
  110. std::function<void()> state_changed;
  111. std::function<void(bool)> initial_read_changed;
  112. std::function<void()> cache_updated;
  113. std::function<void()> poll_cycle_completed;
  114. std::function<void(const std::string &)> error_reported;
  115. std::vector<RegisterAddress> poll_addresses;
  116. std::vector<RegisterWordRange> poll_ranges;
  117. };
  118. using TestSupport::require;
  119. void testUserRuntimePlcRecoveryPolicy()
  120. {
  121. UserRuntimePlcRecoveryPolicy policy;
  122. require(
  123. policy.shouldReconnectSerial(PlcConnectionState::Disconnected),
  124. "a disconnected local serial port must use a full reconnect");
  125. require(
  126. !policy.shouldReconnectSerial(PlcConnectionState::Faulted),
  127. "a PLC communication fault must keep the serial session for recovery probing");
  128. require(
  129. policy.beginFailureEpisode() && policy.failureEpisodeActive(),
  130. "the first error in one communication outage must be reported");
  131. require(
  132. !policy.beginFailureEpisode(),
  133. "later error changes in the same outage must not be reported again");
  134. policy.observeStatus(PlcConnectionState::Connected, false);
  135. require(
  136. policy.failureEpisodeActive(),
  137. "opening the serial port without a complete PLC read must not end the outage");
  138. policy.observeStatus(PlcConnectionState::Recovering, true);
  139. require(
  140. policy.failureEpisodeActive(),
  141. "a recovering connection must remain in the same outage until fully connected");
  142. policy.observeStatus(PlcConnectionState::Connected, true);
  143. require(
  144. !policy.failureEpisodeActive() && policy.beginFailureEpisode(),
  145. "a connected PLC with a complete first read must allow a later outage to report once");
  146. }
  147. void setConditionPath(LadderRung *rung, LogicNode node)
  148. {
  149. for (int column = 0;
  150. column < ProjectLimits::kMaximumConditionColumns;
  151. ++column)
  152. {
  153. rung->cells.push_back({
  154. rung->id + "-cell-" + std::to_string(column),
  155. column == 0 ? LadderCellKind::Node : LadderCellKind::Wire,
  156. column == 0 ? std::optional<LogicNode>{node} : std::nullopt});
  157. }
  158. }
  159. void testModeTransitions()
  160. {
  161. // 验证服务将 PLC 首读状态与领域模式切换规则正确组合
  162. TestProjectStorage storage;
  163. ProjectService project_service(storage, defaultProjectLimitSettings());
  164. VirtualRegisterRepository virtual_repository;
  165. VirtualRegisterRepository plc_repository;
  166. ActiveRegisterRepository active_repository(virtual_repository);
  167. OfflineSimulationService simulation_service(virtual_repository);
  168. OnlineLogicMonitorService online_monitor_service(plc_repository);
  169. LogicEditorService logic_editor_service(project_service);
  170. ReadyPlcGateway gateway;
  171. RuntimeModeService service(
  172. project_service,
  173. logic_editor_service,
  174. simulation_service,
  175. online_monitor_service);
  176. service.configurePlc(
  177. gateway, active_repository, virtual_repository, plc_repository);
  178. Project &project = project_service.editProject();
  179. HmiPage page;
  180. page.id = "runtime-page";
  181. page.name = "Runtime";
  182. HmiControl int32_display;
  183. int32_display.id = "runtime-int32";
  184. int32_display.type = HmiControlType::NumericDisplay;
  185. int32_display.bounds = {0, 0, 100, 40};
  186. int32_display.binding = RegisterAddress{RegisterArea::D, 50};
  187. int32_display.dataType = RegisterDataType::Int32;
  188. page.controls.push_back(int32_display);
  189. HmiControl double_display = int32_display;
  190. double_display.id = "runtime-double";
  191. double_display.bounds = {120, 0, 100, 40};
  192. double_display.binding = RegisterAddress{RegisterArea::D, 60};
  193. double_display.dataType = RegisterDataType::Float64;
  194. page.controls.push_back(double_display);
  195. HmiControl double_status = int32_display;
  196. double_status.id = "runtime-double-status";
  197. double_status.type = HmiControlType::StatusText;
  198. double_status.bounds = {240, 0, 140, 40};
  199. double_status.binding = RegisterAddress{RegisterArea::D, 70};
  200. double_status.dataType = RegisterDataType::Float64;
  201. double_status.statusText = HmiStatusWordTextConfig{{
  202. {std::nullopt, 0.0, "Low"},
  203. {0.0, std::nullopt, "High"}}};
  204. page.controls.push_back(double_status);
  205. HmiControl gated_button;
  206. gated_button.id = "runtime-gated-button";
  207. gated_button.type = HmiControlType::Button;
  208. gated_button.bounds = {400, 0, 120, 40};
  209. gated_button.binding = RegisterAddress{RegisterArea::M, 30};
  210. gated_button.buttonEnableCondition = HmiButtonWordEnableCondition{
  211. RegisterAddress{RegisterArea::D, 80},
  212. RegisterDataType::Int32,
  213. HmiButtonConditionOperator::GreaterThanOrEqual,
  214. 1.0};
  215. page.controls.push_back(gated_button);
  216. project.initialHmiPageId = page.id;
  217. project.hmiPages.push_back(page);
  218. project.alarmDefinitions.push_back(
  219. {"alarm-m", {RegisterArea::M, 12}, AlarmCondition::MOn, 0, "M alarm"});
  220. project.alarmDefinitions.push_back(
  221. {"alarm-d", {RegisterArea::D, 34}, AlarmCondition::DHigh, 100, "D alarm"});
  222. project.registerComments = {
  223. {RegisterAddress{RegisterArea::M, 3999}, "只用于说明"},
  224. {RegisterAddress{RegisterArea::D, 3999}, "只用于说明"}};
  225. ControlLogic logic;
  226. logic.id = "poll-logic";
  227. logic.name = "Poll logic";
  228. LogicNode edge;
  229. edge.id = "poll-edge";
  230. edge.config = EdgeContactNodeConfig{
  231. RegisterAddress{RegisterArea::M, 20}, EdgeMode::Rising};
  232. LogicNode edge_coil;
  233. edge_coil.id = "poll-edge-coil";
  234. edge_coil.config = CoilNodeConfig{
  235. RegisterAddress{RegisterArea::M, 21}, CoilMode::Normal};
  236. LadderRung edge_rung;
  237. edge_rung.id = "poll-edge-rung";
  238. edge_rung.name = "Poll edge";
  239. setConditionPath(&edge_rung, edge);
  240. edge_rung.output = edge_coil;
  241. logic.rungs.push_back(edge_rung);
  242. LogicNode comparison;
  243. comparison.id = "poll-comparison";
  244. comparison.config = CompareNodeConfig{
  245. RegisterAddress{RegisterArea::D, 35}, ComparisonOperator::GreaterThan, 0};
  246. LogicNode comparison_coil;
  247. comparison_coil.id = "poll-comparison-coil";
  248. comparison_coil.config = CoilNodeConfig{
  249. RegisterAddress{RegisterArea::M, 22}, CoilMode::Normal};
  250. LadderRung comparison_rung;
  251. comparison_rung.id = "poll-comparison-rung";
  252. comparison_rung.name = "Poll comparison";
  253. setConditionPath(&comparison_rung, comparison);
  254. comparison_rung.output = comparison_coil;
  255. logic.rungs.push_back(comparison_rung);
  256. LogicNode move_input;
  257. move_input.id = "poll-move-input";
  258. move_input.config = ContactNodeConfig{
  259. RegisterAddress{RegisterArea::M, 27}, ContactMode::NormallyOpen};
  260. LogicNode move_output;
  261. move_output.id = "poll-move";
  262. move_output.config = MoveNodeConfig{
  263. WordOperand{
  264. WordOperandKind::Register,
  265. RegisterAddress{RegisterArea::D, 38},
  266. 0},
  267. RegisterAddress{RegisterArea::D, 39}};
  268. LadderRung move_rung;
  269. move_rung.id = "poll-move-rung";
  270. move_rung.name = "Poll MOVE";
  271. setConditionPath(&move_rung, move_input);
  272. move_rung.output = move_output;
  273. logic.rungs.push_back(move_rung);
  274. LogicNode add_input;
  275. add_input.id = "poll-add-input";
  276. add_input.config = ContactNodeConfig{
  277. RegisterAddress{RegisterArea::M, 28}, ContactMode::NormallyOpen};
  278. LogicNode add_output;
  279. add_output.id = "poll-add";
  280. add_output.config = ArithmeticNodeConfig{
  281. ArithmeticOperation::Add,
  282. WordOperand{
  283. WordOperandKind::Register,
  284. RegisterAddress{RegisterArea::D, 40},
  285. 0},
  286. WordOperand{
  287. WordOperandKind::Constant,
  288. RegisterAddress{RegisterArea::D, 0},
  289. 1},
  290. RegisterAddress{RegisterArea::D, 41}};
  291. LadderRung add_rung;
  292. add_rung.id = "poll-add-rung";
  293. add_rung.name = "Poll ADD";
  294. setConditionPath(&add_rung, add_input);
  295. add_rung.output = add_output;
  296. logic.rungs.push_back(add_rung);
  297. project.controlLogics.push_back(logic);
  298. require(service.mode() == ApplicationMode::Editing,
  299. "service must start in editing mode");
  300. require(service.policy().allowsProjectEditing,
  301. "editing mode must allow project editing");
  302. require(service.enterOfflineRunning().succeeded,
  303. "editing mode must enter offline running");
  304. require(service.simulationState() == SimulationState::Running,
  305. "offline mode must start the software executor");
  306. require(service.policy().usesVirtualRegisters,
  307. "offline running must use virtual registers");
  308. require(service.enterOnlineRunning().error
  309. == ModeTransitionError::MustReturnToEditing,
  310. "running modes must not switch directly");
  311. require(service.enterEditing().succeeded,
  312. "offline running must return to editing");
  313. require(service.simulationState() == SimulationState::Stopped,
  314. "returning to editing must stop the software executor first");
  315. require(service.enterOnlineRunning().error
  316. == ModeTransitionError::InitialPlcReadRequired,
  317. "online running must require an initial PLC read");
  318. require(service.connectPlc(
  319. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  320. "PLC connection must be established before its initial read can complete");
  321. require(gateway.pollAddresses()
  322. == std::vector<RegisterAddress>({
  323. {RegisterArea::M, 12}, {RegisterArea::M, 20},
  324. {RegisterArea::M, 21}, {RegisterArea::M, 22},
  325. {RegisterArea::M, 27}, {RegisterArea::M, 28},
  326. {RegisterArea::M, 30},
  327. {RegisterArea::D, 34}, {RegisterArea::D, 35},
  328. {RegisterArea::D, 38}, {RegisterArea::D, 39},
  329. {RegisterArea::D, 40}, {RegisterArea::D, 41},
  330. {RegisterArea::D, 50}, {RegisterArea::D, 51},
  331. {RegisterArea::D, 60}, {RegisterArea::D, 61},
  332. {RegisterArea::D, 62}, {RegisterArea::D, 63},
  333. {RegisterArea::D, 70}, {RegisterArea::D, 71},
  334. {RegisterArea::D, 72}, {RegisterArea::D, 73},
  335. {RegisterArea::D, 80}, {RegisterArea::D, 81}})
  336. && gateway.pollRanges()
  337. == std::vector<RegisterWordRange>({
  338. {{RegisterArea::D, 50}, 2},
  339. {{RegisterArea::D, 60}, 4},
  340. {{RegisterArea::D, 70}, 4},
  341. {{RegisterArea::D, 80}, 2}}),
  342. "instructions and multi-word status text must be fully polled while comments stay metadata-only");
  343. gateway.completeInitialRead();
  344. require(service.initialPlcReadCompleted(),
  345. "service must retain the initial PLC read state");
  346. plc_repository.writeBit({RegisterArea::M, 20}, true);
  347. require(service.enterOnlineRunning().succeeded,
  348. "online running must start after an initial PLC read");
  349. require(service.simulationState() == SimulationState::Stopped,
  350. "online running must keep the offline executor stopped");
  351. require(service.onlineLogicMonitorService().state()
  352. == OnlineLogicMonitorState::Running,
  353. "online running must start the local read-only trace executor");
  354. require(!plc_repository.readBit({RegisterArea::M, 21}).value,
  355. "the local trace output must not change the PLC source repository");
  356. require(service.policy().usesPlcRegisters,
  357. "online running must use PLC registers");
  358. require(service.policy().runsLogicExecutor,
  359. "online running must advertise the local read-only trace executor");
  360. const std::uint64_t scan_count = service.onlineLogicMonitorService()
  361. .successfulScanCount();
  362. gateway.completePollCycle();
  363. require(service.onlineLogicMonitorService().successfulScanCount()
  364. == scan_count + 1U,
  365. "a completed PLC poll cycle must trigger one new local trace scan");
  366. }
  367. void testHmiOnlyRuntimeSkipsLogicExecutor()
  368. {
  369. TestProjectStorage storage;
  370. ProjectService project_service(storage, defaultProjectLimitSettings());
  371. VirtualRegisterRepository virtual_repository;
  372. PlcRegisterRepository plc_repository;
  373. ActiveRegisterRepository active_repository(virtual_repository);
  374. OfflineSimulationService simulation_service(virtual_repository);
  375. OnlineLogicMonitorService online_monitor_service(plc_repository);
  376. LogicEditorService logic_editor_service(project_service);
  377. ReadyPlcGateway gateway;
  378. RuntimeModeService service(
  379. project_service,
  380. logic_editor_service,
  381. simulation_service,
  382. online_monitor_service);
  383. service.configurePlc(
  384. gateway, active_repository, virtual_repository, plc_repository);
  385. service.setHmiOnlyRuntime(true);
  386. require(!active_repository.readBit({RegisterArea::M, 0}).succeeded,
  387. "HMI-only runtime must use the unavailable PLC cache before its first read");
  388. HmiPage page;
  389. page.id = "hmi-only-page";
  390. page.name = "HMI only";
  391. HmiControl button;
  392. button.id = "hmi-only-button";
  393. button.type = HmiControlType::Button;
  394. button.bounds = {0, 0, 100, 40};
  395. button.binding = RegisterAddress{RegisterArea::M, 0};
  396. page.controls.push_back(button);
  397. Project &project = project_service.editProject();
  398. project.hmiPages.push_back(page);
  399. project.initialHmiPageId = page.id;
  400. // 该逻辑故意不完整,HMI 专用导出不应受它阻断
  401. project.controlLogics.push_back(ControlLogic{});
  402. require(service.connectPlc(
  403. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  404. "HMI-only runtime must accept a PLC connection request");
  405. gateway.completeInitialRead();
  406. plc_repository.updateBit(0, false);
  407. require(service.enterOnlineRunning().succeeded,
  408. "HMI-only runtime must ignore ladder logic readiness");
  409. require(service.policy().usesPlcRegisters
  410. && online_monitor_service.state()
  411. == OnlineLogicMonitorState::Stopped,
  412. "HMI-only runtime must use PLC registers without starting local logic");
  413. require(active_repository.readBit({RegisterArea::M, 0}).succeeded,
  414. "HMI-only runtime must switch HMI reads to the PLC cache");
  415. plc_repository.updateBit(0, true);
  416. require(virtual_repository.writeBit({RegisterArea::M, 0}, false).succeeded
  417. && active_repository.readBit({RegisterArea::M, 0}).value,
  418. "HMI-only runtime fault fixture must separate virtual and PLC values");
  419. plc_repository.invalidate();
  420. gateway.reportFault();
  421. require(service.mode() == ApplicationMode::OnlineRunning
  422. && !service.initialPlcReadCompleted()
  423. && active_repository.readBit({RegisterArea::M, 0}).error
  424. == RegisterError::Unavailable,
  425. "HMI-only runtime faults must keep the PLC cache active without falling back offline");
  426. }
  427. void testMonitorPollRangeRollback()
  428. {
  429. TestProjectStorage storage;
  430. ProjectService project_service(storage, defaultProjectLimitSettings());
  431. VirtualRegisterRepository virtual_repository;
  432. VirtualRegisterRepository plc_repository;
  433. ActiveRegisterRepository active_repository(virtual_repository);
  434. OfflineSimulationService simulation_service(virtual_repository);
  435. OnlineLogicMonitorService online_monitor_service(plc_repository);
  436. LogicEditorService logic_editor_service(project_service);
  437. ReadyPlcGateway gateway;
  438. RuntimeModeService service(
  439. project_service,
  440. logic_editor_service,
  441. simulation_service,
  442. online_monitor_service);
  443. service.configurePlc(
  444. gateway, active_repository, virtual_repository, plc_repository);
  445. gateway.reject_poll_configuration = true;
  446. const PlcCommunicationResult rejected = service.setMonitorAddresses(
  447. {{RegisterArea::D, 100}, {RegisterArea::D, 101},
  448. {RegisterArea::D, 102}, {RegisterArea::D, 103}},
  449. {{{RegisterArea::D, 100}, 4}});
  450. require(!rejected.succeeded,
  451. "a gateway-rejected Double monitor range must fail atomically");
  452. gateway.reject_poll_configuration = false;
  453. require(service.refreshPlcPollAddresses().succeeded
  454. && gateway.pollAddresses().empty()
  455. && gateway.pollRanges().empty(),
  456. "a rejected monitor candidate must not remain in runtime poll state");
  457. }
  458. void testDisconnectedOutputBlocksOfflineAndOnlineRuntime()
  459. {
  460. TestProjectStorage storage;
  461. ProjectService project_service(storage, defaultProjectLimitSettings());
  462. VirtualRegisterRepository virtual_repository;
  463. VirtualRegisterRepository plc_repository;
  464. ActiveRegisterRepository active_repository(virtual_repository);
  465. OfflineSimulationService simulation_service(virtual_repository);
  466. OnlineLogicMonitorService online_monitor_service(plc_repository);
  467. LogicEditorService logic_editor_service(project_service);
  468. ReadyPlcGateway gateway;
  469. RuntimeModeService service(
  470. project_service,
  471. logic_editor_service,
  472. simulation_service,
  473. online_monitor_service);
  474. service.configurePlc(
  475. gateway, active_repository, virtual_repository, plc_repository);
  476. ControlLogic logic;
  477. logic.id = "broken-logic";
  478. logic.name = "断路逻辑";
  479. LadderRung rung;
  480. rung.id = "broken-rung";
  481. rung.name = "行 1";
  482. for (int column = 0;
  483. column < ProjectLimits::kMaximumConditionColumns;
  484. ++column)
  485. {
  486. rung.cells.push_back({
  487. "broken-cell-" + std::to_string(column),
  488. LadderCellKind::Gap,
  489. std::nullopt});
  490. }
  491. rung.output = LogicNode{
  492. "broken-output",
  493. CoilNodeConfig{
  494. RegisterAddress{RegisterArea::M, 60}, CoilMode::Normal},
  495. true};
  496. logic.rungs.push_back(rung);
  497. LadderRung unused;
  498. unused.id = "unused-rung";
  499. unused.name = "行 2";
  500. for (int column = 0;
  501. column < ProjectLimits::kMaximumConditionColumns;
  502. ++column)
  503. {
  504. unused.cells.push_back({
  505. "unused-cell-" + std::to_string(column),
  506. column >= 3 && column <= 5
  507. ? LadderCellKind::Wire : LadderCellKind::Gap,
  508. std::nullopt});
  509. }
  510. logic.rungs.push_back(unused);
  511. project_service.editProject().controlLogics.push_back(logic);
  512. logic_editor_service.clearHistory();
  513. const ModeTransitionResult offline = service.enterOfflineRunning();
  514. require(
  515. !offline.succeeded
  516. && offline.error == ModeTransitionError::ProjectNotReady
  517. && offline.detail.find("断路逻辑") != std::string::npos
  518. && offline.detail.find("第 11 列") != std::string::npos
  519. && offline.detail.find("第 1 列") != std::string::npos
  520. && service.mode() == ApplicationMode::Editing
  521. && simulation_service.state() == SimulationState::Stopped
  522. && service.lastSyntaxCheck().changed
  523. && service.lastSyntaxCheck().removedWireCells == 3U
  524. && logic_editor_service.findCell(
  525. "broken-logic", "unused-rung", 3)->kind
  526. == LadderCellKind::Gap
  527. && logic_editor_service.canUndo(),
  528. "runtime preflight must normalize unused lines before rejecting a broken output");
  529. require(service.connectPlc(
  530. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  531. "the online connectivity check requires a ready PLC cache");
  532. gateway.completeInitialRead();
  533. const ModeTransitionResult online = service.enterOnlineRunning();
  534. require(
  535. !online.succeeded
  536. && online.error == ModeTransitionError::ProjectNotReady
  537. && online.detail == offline.detail
  538. && service.mode() == ApplicationMode::Editing
  539. && online_monitor_service.state()
  540. == OnlineLogicMonitorState::Stopped
  541. && !service.lastSyntaxCheck().changed
  542. && logic_editor_service.undo().succeeded
  543. && logic_editor_service.findCell(
  544. "broken-logic", "unused-rung", 3)->kind
  545. == LadderCellKind::Wire,
  546. "the same disconnected output validation must block online runtime");
  547. }
  548. } // namespace
  549. int main()
  550. {
  551. return TestSupport::runTestSuite("runtime mode service tests", {
  552. {"testUserRuntimePlcRecoveryPolicy", testUserRuntimePlcRecoveryPolicy},
  553. {"testModeTransitions", testModeTransitions},
  554. {"testHmiOnlyRuntimeSkipsLogicExecutor", testHmiOnlyRuntimeSkipsLogicExecutor},
  555. {"testMonitorPollRangeRollback", testMonitorPollRangeRollback},
  556. {"testDisconnectedOutputBlocksOfflineAndOnlineRuntime", testDisconnectedOutputBlocksOfflineAndOnlineRuntime},
  557. });
  558. }