综合平台编程器项目的远程存储
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

275 satır
9.9 KiB

  1. #include "domain/control_logic_model.h"
  2. #include "domain/hmi_model.h"
  3. #include "domain/project_model.h"
  4. #include "domain/register_address.h"
  5. #include "domain/register_repository.h"
  6. #include "domain/runtime_state.h"
  7. #include <cstdint>
  8. #include <exception>
  9. #include <iostream>
  10. #include <stdexcept>
  11. #include <string>
  12. namespace {
  13. void require(bool condition, const std::string &message)
  14. {
  15. if (!condition)
  16. {
  17. throw std::runtime_error(message);
  18. }
  19. }
  20. void testRegisterAddressBoundaries()
  21. {
  22. // 覆盖 M/D 地址允许范围及未知枚举值的拒绝路径
  23. require(RegisterAddress{RegisterArea::M, 0}.isValid(),
  24. "M0 must be valid");
  25. require(RegisterAddress{RegisterArea::D, 4000}.isValid(),
  26. "D4000 must be valid");
  27. require(!(RegisterAddress{RegisterArea::M, -1}.isValid()),
  28. "negative register index must be rejected");
  29. require(!(RegisterAddress{RegisterArea::D, 4001}.isValid()),
  30. "register index above 4000 must be rejected");
  31. require(!(RegisterAddress{static_cast<RegisterArea>(99), 0}.isValid()),
  32. "unknown register area must be rejected");
  33. }
  34. void testRegisterRepositorySeparatesAreas()
  35. {
  36. // 验证离线仓库不会把 M 位和 D 字交叉解释
  37. VirtualRegisterRepository repository;
  38. const RegisterAddress m0{RegisterArea::M, 0};
  39. const RegisterAddress d0{RegisterArea::D, 0};
  40. require(repository.writeBit(m0, true).succeeded, "M bit write must succeed");
  41. require(repository.readBit(m0).value, "M bit read must return written value");
  42. require(repository.writeWord(d0, static_cast<std::int16_t>(-123)).succeeded,
  43. "D word write must succeed");
  44. require(repository.readWord(d0).value == -123, "D word read must return written value");
  45. require(repository.readBit(d0).error == RegisterError::AreaMismatch,
  46. "D address must not be read as a bit");
  47. require(repository.readWord(m0).error == RegisterError::AreaMismatch,
  48. "M address must not be read as a word");
  49. }
  50. Project makeValidProject()
  51. {
  52. // 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线
  53. HmiControl start_button;
  54. start_button.id = "start-button";
  55. start_button.type = HmiControlType::Button;
  56. start_button.text = "Start";
  57. start_button.binding = {RegisterArea::M, 0};
  58. HmiPage page;
  59. page.id = "main-page";
  60. page.name = "Main";
  61. page.controls.push_back(start_button);
  62. LogicNode contact;
  63. contact.id = "start-contact";
  64. contact.config = ContactNodeConfig{
  65. RegisterAddress{RegisterArea::M, 0},
  66. ContactMode::NormallyOpen};
  67. LogicNode coil;
  68. coil.id = "run-coil";
  69. coil.config = CoilNodeConfig{
  70. RegisterAddress{RegisterArea::M, 1},
  71. CoilMode::Normal};
  72. ControlLogic logic;
  73. logic.id = "start-logic";
  74. logic.name = "Start logic";
  75. LadderStage stage;
  76. stage.id = "stage-1";
  77. stage.branches.push_back(contact);
  78. LadderRung rung;
  79. rung.id = "rung-1";
  80. rung.name = "Network 1";
  81. rung.stages.push_back(stage);
  82. rung.output = coil;
  83. logic.rungs.push_back(rung);
  84. Project project;
  85. project.metadata = {"sample-project", "Sample project", "1.0"};
  86. project.hmiPages.push_back(page);
  87. project.controlLogics.push_back(logic);
  88. return project;
  89. }
  90. void testLogicNodeConfigurationBoundaries()
  91. {
  92. // 触点只能绑定 M 区,数值比较只能绑定 D 区
  93. LogicNode contact;
  94. contact.id = "contact";
  95. contact.config = ContactNodeConfig{
  96. RegisterAddress{RegisterArea::M, 0},
  97. ContactMode::NormallyOpen};
  98. require(contact.validate(), "contact node bound to M address must be valid");
  99. contact.config = ContactNodeConfig{
  100. RegisterAddress{RegisterArea::D, 0},
  101. ContactMode::NormallyOpen};
  102. require(!contact.validate(), "contact node bound to D address must be rejected");
  103. LogicNode comparison;
  104. comparison.id = "comparison";
  105. comparison.config = CompareNodeConfig{
  106. RegisterAddress{RegisterArea::D, 0},
  107. ComparisonOperator::GreaterThan,
  108. static_cast<std::int16_t>(100)};
  109. require(comparison.validate(), "comparison node bound to D address must be valid");
  110. }
  111. void testLadderLogicBoundaries()
  112. {
  113. LogicNode stop;
  114. stop.id = "stop";
  115. stop.config = ContactNodeConfig{
  116. RegisterAddress{RegisterArea::M, 1},
  117. ContactMode::NormallyClosed};
  118. LogicNode start;
  119. start.id = "start";
  120. start.config = ContactNodeConfig{
  121. RegisterAddress{RegisterArea::M, 0},
  122. ContactMode::NormallyOpen};
  123. LogicNode run_contact;
  124. run_contact.id = "run-contact";
  125. run_contact.config = ContactNodeConfig{
  126. RegisterAddress{RegisterArea::M, 1},
  127. ContactMode::NormallyOpen};
  128. LogicNode coil;
  129. coil.id = "run-coil";
  130. coil.config = CoilNodeConfig{
  131. RegisterAddress{RegisterArea::M, 1},
  132. CoilMode::Normal};
  133. ControlLogic logic;
  134. logic.id = "hold-logic";
  135. logic.name = "Hold logic";
  136. LadderRung rung;
  137. rung.id = "rung-1";
  138. rung.name = "Self hold";
  139. rung.stages.push_back({"stage-stop", {stop}});
  140. rung.stages.push_back({"stage-start", {start, run_contact}});
  141. rung.output = coil;
  142. logic.rungs.push_back(rung);
  143. require(logic.validate(), "stop AND (start OR run) self-hold ladder must be valid");
  144. logic.rungs.front().stages.front().branches.push_back(coil);
  145. require(!logic.validate(), "a ladder condition stage must reject coils");
  146. logic.rungs.front().stages.front().branches.pop_back();
  147. logic.rungs.front().output = start;
  148. require(!logic.validate(), "a ladder output must be a coil");
  149. logic.rungs.front().output.reset();
  150. require(logic.validate(), "incomplete ladder may remain in an editable draft");
  151. require(!logic.validateForRunning(),
  152. "conditions without an output must block runtime validation");
  153. LadderRung empty_rung{"rung-empty", "Empty network", {}, std::nullopt};
  154. require(empty_rung.validate(), "an empty editing network must be valid");
  155. empty_rung.output = coil;
  156. require(empty_rung.validate(), "output-only network may remain in an editable draft");
  157. require(!empty_rung.validateForRunning(),
  158. "an output without conditions must block runtime validation");
  159. logic.rungs.front().output = coil;
  160. logic.rungs.front().stages.at(1).branches.at(1).id = start.id;
  161. require(!logic.validate(), "logic node ids must be unique");
  162. }
  163. void testModelsValidateBindingsAndIdentifiers()
  164. {
  165. // 聚合验证必须拒绝错误绑定、重复标识和越界控件
  166. Project project = makeValidProject();
  167. require(project.validate(), "valid project model must pass validation");
  168. project.hmiPages.front().controls.front().binding =
  169. RegisterAddress{RegisterArea::D, 0};
  170. require(!project.validate(), "button bound to D area must be rejected");
  171. project = makeValidProject();
  172. project.hmiPages.push_back(project.hmiPages.front());
  173. require(!project.validate(), "duplicate HMI page id must be rejected");
  174. project = makeValidProject();
  175. project.hmiPages.front().controls.front().bounds.x = -1;
  176. require(!project.validate(), "controls outside the page must be rejected");
  177. project = makeValidProject();
  178. project.hmiPages.front().controls.front().bounds.width = 801;
  179. require(!project.validate(), "controls wider than the page must be rejected");
  180. project = makeValidProject();
  181. project.hmiPages.front().controls.front().properties.emplace("", "value");
  182. require(!project.validate(), "empty HMI property names must be rejected");
  183. project = makeValidProject();
  184. project.hmiPages.front().controls.front().binding.reset();
  185. require(project.validate(), "unbound HMI control must be accepted in a draft");
  186. require(!project.validateForRunning(),
  187. "unbound HMI control must block runtime validation");
  188. project = makeValidProject();
  189. project.controlLogics.front().rungs.front().output->configured = false;
  190. require(project.validate(), "unconfigured ladder node must be accepted in a draft");
  191. require(!project.validateForRunning(),
  192. "unconfigured ladder node must block runtime validation");
  193. }
  194. void testRuntimeStateBoundaries()
  195. {
  196. // 运行模式测试覆盖离线和真机的互斥及 PLC 首读前置条件
  197. RuntimeState state;
  198. require(state.policy().allowsProjectEditing, "editing mode must allow project editing");
  199. require(state.enterOfflineRunning().succeeded, "editing may enter offline running");
  200. require(state.policy().usesVirtualRegisters, "offline mode must use virtual registers");
  201. require(state.policy().runsLogicExecutor, "offline mode must run logic executor");
  202. require(state.enterOnlineRunning(true).error
  203. == ModeTransitionError::MustReturnToEditing,
  204. "offline mode must not directly enter online mode");
  205. require(state.enterEditing().succeeded, "offline mode may return to editing");
  206. require(state.enterOnlineRunning(false).error
  207. == ModeTransitionError::InitialPlcReadRequired,
  208. "online mode must require an initial PLC read");
  209. require(state.enterOnlineRunning(true).succeeded,
  210. "editing may enter online mode after initial PLC read");
  211. require(!state.policy().runsLogicExecutor,
  212. "online mode must keep the software logic executor stopped");
  213. require(state.policy().usesPlcRegisters, "online mode must use PLC registers");
  214. }
  215. } // namespace
  216. int main()
  217. {
  218. try
  219. {
  220. // 每个测试函数独立覆盖一个领域边界,首个异常即终止测试进程
  221. testRegisterAddressBoundaries();
  222. testRegisterRepositorySeparatesAreas();
  223. testLogicNodeConfigurationBoundaries();
  224. testLadderLogicBoundaries();
  225. testModelsValidateBindingsAndIdentifiers();
  226. testRuntimeStateBoundaries();
  227. }
  228. catch (const std::exception &error)
  229. {
  230. std::cerr << "domain tests failed: " << error.what() << '\n';
  231. return 1;
  232. }
  233. std::cout << "domain tests passed\n";
  234. return 0;
  235. }