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

316 line
12 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 testRegisterAddressParsing()
  35. {
  36. const RegisterAddressParseResult m0 = parseRegisterAddress(" m0 ");
  37. require(m0.succeeded && m0.address == RegisterAddress{RegisterArea::M, 0},
  38. "register parser must trim and normalize lowercase M addresses");
  39. const RegisterAddressParseResult d4000 = parseRegisterAddress("D4000");
  40. require(d4000.succeeded
  41. && d4000.address == RegisterAddress{RegisterArea::D, 4000},
  42. "register parser must accept the maximum D address");
  43. require(parseRegisterAddress("").error == RegisterAddressParseError::Empty,
  44. "register parser must distinguish empty input");
  45. require(parseRegisterAddress("X0").error
  46. == RegisterAddressParseError::UnsupportedArea,
  47. "register parser must reject unsupported areas");
  48. require(parseRegisterAddress("M1.0").error
  49. == RegisterAddressParseError::InvalidFormat,
  50. "register parser must reject non-decimal indices");
  51. require(parseRegisterAddress("D4001").error
  52. == RegisterAddressParseError::OutOfRange,
  53. "register parser must reject addresses above the project range");
  54. }
  55. void testRegisterRepositorySeparatesAreas()
  56. {
  57. // 验证离线仓库不会把 M 位和 D 字交叉解释
  58. VirtualRegisterRepository repository;
  59. const RegisterAddress m0{RegisterArea::M, 0};
  60. const RegisterAddress d0{RegisterArea::D, 0};
  61. require(repository.writeBit(m0, true).succeeded, "M bit write must succeed");
  62. require(repository.readBit(m0).value, "M bit read must return written value");
  63. require(repository.writeWord(d0, static_cast<std::int16_t>(-123)).succeeded,
  64. "D word write must succeed");
  65. require(repository.readWord(d0).value == -123, "D word read must return written value");
  66. require(repository.readBit(d0).error == RegisterError::AreaMismatch,
  67. "D address must not be read as a bit");
  68. require(repository.readWord(m0).error == RegisterError::AreaMismatch,
  69. "M address must not be read as a word");
  70. }
  71. Project makeValidProject()
  72. {
  73. // 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线
  74. HmiControl start_button;
  75. start_button.id = "start-button";
  76. start_button.type = HmiControlType::Button;
  77. start_button.text = "Start";
  78. start_button.binding = {RegisterArea::M, 0};
  79. HmiPage page;
  80. page.id = "main-page";
  81. page.name = "Main";
  82. page.controls.push_back(start_button);
  83. LogicNode contact;
  84. contact.id = "start-contact";
  85. contact.config = ContactNodeConfig{
  86. RegisterAddress{RegisterArea::M, 0},
  87. ContactMode::NormallyOpen};
  88. LogicNode coil;
  89. coil.id = "run-coil";
  90. coil.config = CoilNodeConfig{
  91. RegisterAddress{RegisterArea::M, 1},
  92. CoilMode::Normal};
  93. ControlLogic logic;
  94. logic.id = "start-logic";
  95. logic.name = "Start logic";
  96. LadderRung rung;
  97. rung.id = "rung-1";
  98. rung.name = "Network 1";
  99. rung.condition = ConditionExpression::fromNode(contact);
  100. rung.output = coil;
  101. logic.rungs.push_back(rung);
  102. Project project;
  103. project.metadata = {"sample-project", "Sample project", "1.0"};
  104. project.hmiPages.push_back(page);
  105. project.controlLogics.push_back(logic);
  106. return project;
  107. }
  108. void testLogicNodeConfigurationBoundaries()
  109. {
  110. // 触点只能绑定 M 区,数值比较只能绑定 D 区
  111. LogicNode contact;
  112. contact.id = "contact";
  113. contact.config = ContactNodeConfig{
  114. RegisterAddress{RegisterArea::M, 0},
  115. ContactMode::NormallyOpen};
  116. require(contact.validate(), "contact node bound to M address must be valid");
  117. contact.config = ContactNodeConfig{
  118. RegisterAddress{RegisterArea::D, 0},
  119. ContactMode::NormallyOpen};
  120. require(!contact.validate(), "contact node bound to D address must be rejected");
  121. LogicNode comparison;
  122. comparison.id = "comparison";
  123. comparison.config = CompareNodeConfig{
  124. RegisterAddress{RegisterArea::D, 0},
  125. ComparisonOperator::GreaterThan,
  126. static_cast<std::int16_t>(100)};
  127. require(comparison.validate(), "comparison node bound to D address must be valid");
  128. }
  129. void testLadderLogicBoundaries()
  130. {
  131. LogicNode stop;
  132. stop.id = "stop";
  133. stop.config = ContactNodeConfig{
  134. RegisterAddress{RegisterArea::M, 1},
  135. ContactMode::NormallyClosed};
  136. LogicNode start;
  137. start.id = "start";
  138. start.config = ContactNodeConfig{
  139. RegisterAddress{RegisterArea::M, 0},
  140. ContactMode::NormallyOpen};
  141. LogicNode run_contact;
  142. run_contact.id = "run-contact";
  143. run_contact.config = ContactNodeConfig{
  144. RegisterAddress{RegisterArea::M, 1},
  145. ContactMode::NormallyOpen};
  146. LogicNode coil;
  147. coil.id = "run-coil";
  148. coil.config = CoilNodeConfig{
  149. RegisterAddress{RegisterArea::M, 1},
  150. CoilMode::Normal};
  151. ControlLogic logic;
  152. logic.id = "hold-logic";
  153. logic.name = "Hold logic";
  154. ConditionExpression start_parallel;
  155. start_parallel.id = "parallel-start";
  156. start_parallel.kind = ConditionExpressionKind::Parallel;
  157. start_parallel.children = {
  158. ConditionExpression::fromNode(start),
  159. ConditionExpression::fromNode(run_contact)};
  160. ConditionExpression root;
  161. root.id = "series-root";
  162. root.kind = ConditionExpressionKind::Series;
  163. root.children = {
  164. ConditionExpression::fromNode(stop),
  165. start_parallel};
  166. LadderRung rung;
  167. rung.id = "rung-1";
  168. rung.name = "Self hold";
  169. rung.condition = root;
  170. rung.output = coil;
  171. logic.rungs.push_back(rung);
  172. require(logic.validate(), "stop AND (start OR run) self-hold ladder must be valid");
  173. logic.rungs.front().condition->children.front() =
  174. ConditionExpression::fromNode(coil);
  175. require(!logic.validate(), "a ladder condition expression must reject coils");
  176. logic.rungs.front().condition = root;
  177. logic.rungs.front().output = start;
  178. require(!logic.validate(), "a ladder output must be a coil");
  179. logic.rungs.front().output.reset();
  180. require(logic.validate(), "incomplete ladder may remain in an editable draft");
  181. require(!logic.validateForRunning(),
  182. "conditions without an output must block runtime validation");
  183. LadderRung empty_rung{"rung-empty", "Empty network", std::nullopt, std::nullopt};
  184. require(empty_rung.validate(), "an empty editing network must be valid");
  185. empty_rung.output = coil;
  186. require(empty_rung.validate(), "output-only network may remain in an editable draft");
  187. require(!empty_rung.validateForRunning(),
  188. "an output without conditions must block runtime validation");
  189. logic.rungs.front().output = coil;
  190. logic.rungs.front().condition = root;
  191. logic.rungs.front().condition->children.at(1).children.at(1).node->id = start.id;
  192. require(!logic.validate(), "logic node ids must be unique");
  193. ConditionExpression nested_parallel;
  194. nested_parallel.id = "parallel-nested";
  195. nested_parallel.kind = ConditionExpressionKind::Parallel;
  196. nested_parallel.children = {
  197. ConditionExpression::fromNode(start),
  198. root};
  199. require(nested_parallel.validate(),
  200. "nested series and parallel expressions must be valid");
  201. }
  202. void testModelsValidateBindingsAndIdentifiers()
  203. {
  204. // 聚合验证必须拒绝错误绑定、重复标识和越界控件
  205. Project project = makeValidProject();
  206. require(project.validate(), "valid project model must pass validation");
  207. project.hmiPages.front().controls.front().binding =
  208. RegisterAddress{RegisterArea::D, 0};
  209. require(!project.validate(), "button bound to D area must be rejected");
  210. project = makeValidProject();
  211. project.hmiPages.push_back(project.hmiPages.front());
  212. require(!project.validate(), "duplicate HMI page id must be rejected");
  213. project = makeValidProject();
  214. project.hmiPages.front().controls.front().bounds.x = -1;
  215. require(!project.validate(), "controls outside the page must be rejected");
  216. project = makeValidProject();
  217. project.hmiPages.front().controls.front().bounds.width = 801;
  218. require(!project.validate(), "controls wider than the page must be rejected");
  219. project = makeValidProject();
  220. project.hmiPages.front().controls.front().properties.emplace("", "value");
  221. require(!project.validate(), "empty HMI property names must be rejected");
  222. project = makeValidProject();
  223. project.hmiPages.front().controls.front().binding.reset();
  224. require(project.validate(), "unbound HMI control must be accepted in a draft");
  225. require(!project.validateForRunning(),
  226. "unbound HMI control must block runtime validation");
  227. project = makeValidProject();
  228. project.controlLogics.front().rungs.front().output->configured = false;
  229. require(project.validate(), "unconfigured ladder node must be accepted in a draft");
  230. require(!project.validateForRunning(),
  231. "unconfigured ladder node must block runtime validation");
  232. }
  233. void testRuntimeStateBoundaries()
  234. {
  235. // 运行模式测试覆盖离线和真机的互斥及 PLC 首读前置条件
  236. RuntimeState state;
  237. require(state.policy().allowsProjectEditing, "editing mode must allow project editing");
  238. require(state.enterOfflineRunning().succeeded, "editing may enter offline running");
  239. require(state.policy().usesVirtualRegisters, "offline mode must use virtual registers");
  240. require(state.policy().runsLogicExecutor, "offline mode must run logic executor");
  241. require(state.enterOnlineRunning(true).error
  242. == ModeTransitionError::MustReturnToEditing,
  243. "offline mode must not directly enter online mode");
  244. require(state.enterEditing().succeeded, "offline mode may return to editing");
  245. require(state.enterOnlineRunning(false).error
  246. == ModeTransitionError::InitialPlcReadRequired,
  247. "online mode must require an initial PLC read");
  248. require(state.enterOnlineRunning(true).succeeded,
  249. "editing may enter online mode after initial PLC read");
  250. require(!state.policy().runsLogicExecutor,
  251. "online mode must keep the software logic executor stopped");
  252. require(state.policy().usesPlcRegisters, "online mode must use PLC registers");
  253. }
  254. } // namespace
  255. int main()
  256. {
  257. try
  258. {
  259. // 每个测试函数独立覆盖一个领域边界,首个异常即终止测试进程
  260. testRegisterAddressBoundaries();
  261. testRegisterAddressParsing();
  262. testRegisterRepositorySeparatesAreas();
  263. testLogicNodeConfigurationBoundaries();
  264. testLadderLogicBoundaries();
  265. testModelsValidateBindingsAndIdentifiers();
  266. testRuntimeStateBoundaries();
  267. }
  268. catch (const std::exception &error)
  269. {
  270. std::cerr << "domain tests failed: " << error.what() << '\n';
  271. return 1;
  272. }
  273. std::cout << "domain tests passed\n";
  274. return 0;
  275. }