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

1153 рядки
50 KiB

  1. #include "domain/control_logic_model.h"
  2. #include "domain/hmi_control_registry.h"
  3. #include "domain/hmi_model.h"
  4. #include "domain/project_model.h"
  5. #include "domain/project_limits.h"
  6. #include "domain/register_address.h"
  7. #include "domain/register_repository.h"
  8. #include "domain/runtime_state.h"
  9. #include "domain/virtual_register_repository.h"
  10. #include "support/test_support.h"
  11. #include <algorithm>
  12. #include <cmath>
  13. #include <cstdint>
  14. #include <exception>
  15. #include <iostream>
  16. #include <limits>
  17. #include <set>
  18. #include <stdexcept>
  19. #include <string>
  20. namespace {
  21. using TestSupport::require;
  22. void testRegisterAddressBoundaries()
  23. {
  24. // 覆盖 M/D 地址允许范围及未知枚举值的拒绝路径
  25. require(RegisterAddress{RegisterArea::M, 0}.isValid(),
  26. "M0 must be valid");
  27. require(RegisterAddress{RegisterArea::D, 4000}.isValid(),
  28. "D4000 must be valid");
  29. require(!(RegisterAddress{RegisterArea::M, -1}.isValid()),
  30. "negative register index must be rejected");
  31. require(!(RegisterAddress{RegisterArea::D, 4001}.isValid()),
  32. "register index above 4000 must be rejected");
  33. require(!(RegisterAddress{static_cast<RegisterArea>(99), 0}.isValid()),
  34. "unknown register area must be rejected");
  35. }
  36. void testRegisterAddressParsing()
  37. {
  38. const RegisterAddressParseResult m0 = parseRegisterAddress(" m0 ");
  39. require(m0.succeeded && m0.address == RegisterAddress{RegisterArea::M, 0},
  40. "register parser must trim and normalize lowercase M addresses");
  41. const RegisterAddressParseResult d4000 = parseRegisterAddress("D4000");
  42. require(d4000.succeeded
  43. && d4000.address == RegisterAddress{RegisterArea::D, 4000},
  44. "register parser must accept the maximum D address");
  45. require(parseRegisterAddress("").error == RegisterAddressParseError::Empty,
  46. "register parser must distinguish empty input");
  47. require(parseRegisterAddress("X0").error
  48. == RegisterAddressParseError::UnsupportedArea,
  49. "register parser must reject unsupported areas");
  50. require(parseRegisterAddress("M1.0").error
  51. == RegisterAddressParseError::InvalidFormat,
  52. "register parser must reject non-decimal indices");
  53. require(parseRegisterAddress("D4001").error
  54. == RegisterAddressParseError::OutOfRange,
  55. "register parser must reject addresses above the project range");
  56. }
  57. void testRegisterRepositorySeparatesAreas()
  58. {
  59. // 验证离线仓库不会把 M 位和 D 字交叉解释
  60. VirtualRegisterRepository repository;
  61. const RegisterAddress m0{RegisterArea::M, 0};
  62. const RegisterAddress d0{RegisterArea::D, 0};
  63. require(repository.writeBit(m0, true).succeeded, "M bit write must succeed");
  64. require(repository.readBit(m0).value, "M bit read must return written value");
  65. require(repository.writeWord(d0, static_cast<std::int16_t>(-123)).succeeded,
  66. "D word write must succeed");
  67. require(repository.readWord(d0).value == -123, "D word read must return written value");
  68. require(repository.readBit(d0).error == RegisterError::AreaMismatch,
  69. "D address must not be read as a bit");
  70. require(repository.readWord(m0).error == RegisterError::AreaMismatch,
  71. "M address must not be read as a word");
  72. }
  73. void testMultiWordCodecsAndBlockAccess()
  74. {
  75. const std::array<std::int32_t, 4> int32_values = {
  76. std::numeric_limits<std::int32_t>::min(), -1, 0,
  77. std::numeric_limits<std::int32_t>::max()};
  78. for (const std::int32_t value : int32_values)
  79. {
  80. const std::array<std::int16_t, 2> words = Int32Codec::encode(value);
  81. require(Int32Codec::decode(words[0], words[1]) == value,
  82. "Int32 codec must preserve signed boundary values");
  83. }
  84. const std::array<std::int16_t, 2> int32_words =
  85. Int32Codec::encode(0x12345678);
  86. require(static_cast<std::uint16_t>(int32_words[0]) == 0x5678U
  87. && static_cast<std::uint16_t>(int32_words[1]) == 0x1234U,
  88. "Int32 codec must store the low 16-bit word at the lower D address");
  89. const std::array<float, 4> values = {1.0f, -2.5f, 0.1f, 0.0f};
  90. for (const float value : values)
  91. {
  92. const std::array<std::int16_t, 2> words = Float32Codec::encode(value);
  93. const std::optional<float> decoded = Float32Codec::decode(words[0], words[1]);
  94. require(decoded.has_value() && *decoded == value,
  95. "Float32 codec must preserve representative finite values");
  96. }
  97. require(!Float32Codec::decode(
  98. static_cast<std::int16_t>(0),
  99. static_cast<std::int16_t>(0x7f80)).has_value(),
  100. "Float32 codec must reject positive infinity");
  101. require(!Float32Codec::decode(
  102. static_cast<std::int16_t>(0),
  103. static_cast<std::int16_t>(0x7fc0)).has_value(),
  104. "Float32 codec must reject NaN");
  105. const std::array<std::int16_t, 2> float20 = Float32Codec::encode(20.0f);
  106. require(static_cast<std::uint16_t>(float20[0]) == 0x0000U
  107. && static_cast<std::uint16_t>(float20[1]) == 0x41a0U,
  108. "Float32 20.0 must match the Xinje low-word-first example");
  109. const std::array<double, 4> double_values = {1.0, -2.5, 0.1, 0.0};
  110. for (const double value : double_values)
  111. {
  112. const std::array<std::int16_t, 4> words = Float64Codec::encode(value);
  113. const std::optional<double> decoded = Float64Codec::decode(words);
  114. require(decoded.has_value() && *decoded == value,
  115. "Double codec must preserve representative finite values");
  116. }
  117. const std::array<std::int16_t, 4> double_one = Float64Codec::encode(1.0);
  118. require(static_cast<std::uint16_t>(double_one[0]) == 0x0000U
  119. && static_cast<std::uint16_t>(double_one[1]) == 0x0000U
  120. && static_cast<std::uint16_t>(double_one[2]) == 0x0000U
  121. && static_cast<std::uint16_t>(double_one[3]) == 0x3ff0U,
  122. "Double 1.0 must occupy four low-address-first D words");
  123. require(!Float64Codec::decode({
  124. static_cast<std::int16_t>(0), static_cast<std::int16_t>(0),
  125. static_cast<std::int16_t>(0), static_cast<std::int16_t>(0x7ff0)})
  126. .has_value()
  127. && !Float64Codec::decode({
  128. static_cast<std::int16_t>(0), static_cast<std::int16_t>(0),
  129. static_cast<std::int16_t>(0), static_cast<std::int16_t>(0x7ff8)})
  130. .has_value(),
  131. "Double codec must reject infinity and NaN bit patterns");
  132. require(encodeRegisterNumericValue(
  133. RegisterDataType::Int32,
  134. static_cast<double>(std::numeric_limits<std::int32_t>::min()))
  135. .has_value()
  136. && encodeRegisterNumericValue(
  137. RegisterDataType::Int32,
  138. static_cast<double>(std::numeric_limits<std::int32_t>::max()))
  139. .has_value()
  140. && !encodeRegisterNumericValue(
  141. RegisterDataType::Int32, 1.5).has_value()
  142. && !encodeRegisterNumericValue(
  143. RegisterDataType::Float64,
  144. std::numeric_limits<double>::infinity()).has_value()
  145. && !encodeRegisterNumericValue(
  146. RegisterDataType::Float64,
  147. std::numeric_limits<double>::quiet_NaN()).has_value(),
  148. "typed encoding must enforce Int32 integrality and finite Double values");
  149. VirtualRegisterRepository repository;
  150. const RegisterAddress d10{RegisterArea::D, 10};
  151. const auto float_words = Float32Codec::encode(-2.5f);
  152. require(repository.writeWords(
  153. d10, {float_words[0], float_words[1]}).succeeded,
  154. "virtual repository must write Float32 low and high words");
  155. const WordsReadResult pair = repository.readWords(d10, 2);
  156. require(pair.succeeded
  157. && Float32Codec::decode(pair.values[0], pair.values[1]).value() == -2.5f,
  158. "virtual repository must read Float32 from two consecutive words");
  159. require(!repository.writeWords({RegisterArea::D, 4000}, {}).succeeded,
  160. "Float32 write at D4000 must be rejected");
  161. require(repository.writeWords(
  162. {RegisterArea::D, 30},
  163. {double_one[0], double_one[1], double_one[2], double_one[3]})
  164. .succeeded
  165. && repository.readWords({RegisterArea::D, 30}, 4).values
  166. == std::vector<std::int16_t>(
  167. double_one.cbegin(), double_one.cend()),
  168. "virtual repository must read and write one complete four-word block");
  169. repository.writeWord({RegisterArea::D, 4000}, 77);
  170. require(!repository.writeWords(
  171. {RegisterArea::D, 4000}, {1, 2}).succeeded
  172. && repository.readWord({RegisterArea::D, 4000}).value == 77,
  173. "an overflowing block write must fail before changing any D word");
  174. RegisterDataType parsed = RegisterDataType::Int16;
  175. require(parseRegisterDataType("int32", &parsed)
  176. && parsed == RegisterDataType::Int32
  177. && parseRegisterDataType("float64", &parsed)
  178. && parsed == RegisterDataType::Float64
  179. && registerDataTypeWordCount(RegisterDataType::Float64) == 4,
  180. "register data type descriptors must expose Int32 and Float64 names");
  181. const RegisterDataType invalid_type = static_cast<RegisterDataType>(99);
  182. require(!registerDataTypeIsSupported(invalid_type)
  183. && registerDataTypeWordCount(invalid_type) == 0
  184. && !registerDataTypeAddressIsValid(
  185. invalid_type, {RegisterArea::D, 0}),
  186. "unknown register data types must not borrow Int16 rules");
  187. }
  188. void testHmiControlRegistryCompleteness()
  189. {
  190. std::set<const HmiControlDescriptor *> descriptors;
  191. std::set<std::string> storage_names;
  192. std::set<std::string> id_prefixes;
  193. const std::size_t control_type_count =
  194. static_cast<std::size_t>(HmiControlType::Count);
  195. for (std::size_t index = 0; index < control_type_count; ++index)
  196. {
  197. const HmiControlType type = static_cast<HmiControlType>(index);
  198. const HmiControlDescriptor *descriptor =
  199. findHmiControlDescriptor(type);
  200. require(descriptor != nullptr,
  201. "every HMI control type must have one descriptor");
  202. require(descriptor->type == type,
  203. "HMI type lookup must return the requested descriptor");
  204. require(descriptors.emplace(descriptor).second,
  205. "each HMI control type must resolve to a different descriptor");
  206. require(descriptor->storageName != nullptr
  207. && descriptor->storageName[0] != '\0',
  208. "HMI storage names must not be empty");
  209. require(descriptor->displayName != nullptr
  210. && descriptor->displayName[0] != '\0',
  211. "HMI display names must not be empty");
  212. require(descriptor->idPrefix != nullptr
  213. && descriptor->idPrefix[0] != '\0',
  214. "HMI id prefixes must not be empty");
  215. require(descriptor->defaultText != nullptr,
  216. "HMI default text must not be null");
  217. require(descriptor->defaultBounds.width > 0
  218. && descriptor->defaultBounds.height > 0,
  219. "HMI default bounds must have a positive size");
  220. require(findHmiControlDescriptor(descriptor->storageName) == descriptor,
  221. "HMI storage name lookup must return its registered descriptor");
  222. require(storage_names.emplace(descriptor->storageName).second,
  223. "HMI storage names must be unique");
  224. require(id_prefixes.emplace(descriptor->idPrefix).second,
  225. "HMI id prefixes must be unique");
  226. const std::optional<RegisterArea> binding_area =
  227. hmiBindingArea(descriptor->bindingKind);
  228. switch (descriptor->runtimeValueKind)
  229. {
  230. case HmiRuntimeValueKind::Bit:
  231. {
  232. require(descriptor->bindingKind == HmiBindingKind::Bit
  233. && binding_area == RegisterArea::M,
  234. "bit runtime controls must bind to the M area");
  235. break;
  236. }
  237. case HmiRuntimeValueKind::Word:
  238. {
  239. require(descriptor->bindingKind == HmiBindingKind::Word
  240. && binding_area == RegisterArea::D,
  241. "word runtime controls must bind to the D area");
  242. break;
  243. }
  244. case HmiRuntimeValueKind::BitOrWord:
  245. {
  246. require(descriptor->bindingKind == HmiBindingKind::BitOrWord
  247. && !binding_area.has_value(),
  248. "flexible runtime controls must choose M or D from their config");
  249. break;
  250. }
  251. case HmiRuntimeValueKind::None:
  252. {
  253. require(descriptor->bindingKind == HmiBindingKind::None
  254. && !binding_area.has_value(),
  255. "static controls must not declare a register binding");
  256. break;
  257. }
  258. }
  259. require(descriptor->requiresBindingForRunning
  260. == (descriptor->runtimeValueKind
  261. != HmiRuntimeValueKind::None),
  262. "runtime value controls must require a configured binding");
  263. }
  264. require(findHmiControlDescriptor(HmiControlType::Count) == nullptr,
  265. "the HMI control count marker must not be registered");
  266. require(findHmiControlDescriptor(static_cast<HmiControlType>(99)) == nullptr,
  267. "unknown HMI control types must not resolve");
  268. require(findHmiControlDescriptor("unknown") == nullptr,
  269. "unknown HMI storage names must not resolve");
  270. require(hmiBindingArea(HmiBindingKind::Bit) == RegisterArea::M,
  271. "bit bindings must use the M area");
  272. require(hmiBindingArea(HmiBindingKind::Word) == RegisterArea::D,
  273. "word bindings must use the D area");
  274. require(!hmiBindingArea(HmiBindingKind::None).has_value(),
  275. "controls without bindings must not resolve a register area");
  276. }
  277. Project makeValidProject();
  278. void testStatusTextDomainRules()
  279. {
  280. HmiControl status;
  281. status.id = "status";
  282. status.type = HmiControlType::StatusText;
  283. status.text = "Status";
  284. status.binding = RegisterAddress{RegisterArea::M, 0};
  285. status.statusText = HmiStatusBitTextConfig{"Stopped", "Running"};
  286. require(status.validate(), "M status text with OFF and ON labels must be valid");
  287. status.binding = RegisterAddress{RegisterArea::D, 0};
  288. require(!status.validate(), "M status text configuration must reject a D binding");
  289. status.binding = RegisterAddress{RegisterArea::D, 20};
  290. status.dataType = RegisterDataType::Float32;
  291. status.statusText = HmiStatusWordTextConfig{{
  292. {std::nullopt, 30.0, "Low"},
  293. {30.0, 80.0, "Normal"},
  294. {80.0, std::nullopt, "High"}}};
  295. require(status.validate(),
  296. "D status text must accept complete adjacent half-open ranges");
  297. status.binding.reset();
  298. require(status.validate(),
  299. "an unbound D status text draft must keep its selected data type");
  300. status.binding = RegisterAddress{RegisterArea::D, 20};
  301. const auto consecutive_ranges = [](std::size_t count)
  302. {
  303. std::vector<HmiStatusValueRange> result;
  304. result.reserve(count);
  305. for (std::size_t index = 0; index < count; ++index)
  306. {
  307. result.push_back({
  308. index == 0U
  309. ? std::optional<double>{}
  310. : std::optional<double>{static_cast<double>(index)},
  311. index + 1U == count
  312. ? std::optional<double>{}
  313. : std::optional<double>{static_cast<double>(index + 1U)},
  314. "State"});
  315. }
  316. return result;
  317. };
  318. status.statusText = HmiStatusWordTextConfig{
  319. consecutive_ranges(ProjectLimits::kMaximumStatusTextRanges)};
  320. require(status.validate(), "a status text control must accept 16 ranges");
  321. status.statusText = HmiStatusWordTextConfig{
  322. consecutive_ranges(ProjectLimits::kMaximumStatusTextRanges + 1U)};
  323. require(!status.validate(), "a status text control must reject a 17th range");
  324. status.statusText = HmiStatusWordTextConfig{{
  325. {std::nullopt, 30.0, "Low"},
  326. {30.0, 80.0, "Normal"},
  327. {80.0, std::nullopt, "High"}}};
  328. auto &ranges = std::get<HmiStatusWordTextConfig>(*status.statusText).ranges;
  329. ranges[1].lowerBound = 31.0;
  330. require(!status.validate(), "D status text must reject range gaps");
  331. ranges[1].lowerBound = 29.0;
  332. require(!status.validate(), "D status text must reject overlapping ranges");
  333. ranges[1].lowerBound = 30.0;
  334. ranges.front().lowerBound = -100.0;
  335. require(!status.validate(), "D status text must start with an unlimited lower bound");
  336. ranges.front().lowerBound.reset();
  337. status.dataType = RegisterDataType::Int32;
  338. ranges[1].upperBound = 80.5;
  339. require(!status.validate(), "integer status text boundaries must be integers");
  340. Project project = makeValidProject();
  341. status.dataType = RegisterDataType::Float64;
  342. status.binding = RegisterAddress{RegisterArea::D, 100};
  343. ranges[1].upperBound = 80.0;
  344. project.hmiPages.front().controls.push_back(status);
  345. HmiControl overlapping;
  346. overlapping.id = "overlapping";
  347. overlapping.type = HmiControlType::NumericDisplay;
  348. overlapping.text = "Overlap";
  349. overlapping.binding = RegisterAddress{RegisterArea::D, 102};
  350. project.hmiPages.front().controls.push_back(overlapping);
  351. require(!project.validate(defaultProjectLimitSettings()),
  352. "D status text must participate in multi-word HMI overlap checks");
  353. }
  354. void testMultiWordHmiBoundaries()
  355. {
  356. Project project = makeValidProject();
  357. HmiControl display;
  358. display.id = "float-display";
  359. display.type = HmiControlType::NumericDisplay;
  360. display.text = "Value";
  361. display.binding = RegisterAddress{RegisterArea::D, 3999};
  362. display.dataType = RegisterDataType::Float32;
  363. project.hmiPages.front().controls.push_back(display);
  364. require(project.validate(defaultProjectLimitSettings()),
  365. "Float32 D3999 must be valid and occupy D3999~D4000");
  366. project.hmiPages.front().controls.back().binding =
  367. RegisterAddress{RegisterArea::D, 4000};
  368. require(!project.validate(defaultProjectLimitSettings()), "Float32 D4000 must be rejected");
  369. project = makeValidProject();
  370. HmiControl int32_display = display;
  371. int32_display.id = "int32-display";
  372. int32_display.dataType = RegisterDataType::Int32;
  373. int32_display.binding = RegisterAddress{RegisterArea::D, 3999};
  374. project.hmiPages.front().controls.push_back(int32_display);
  375. require(project.validate(defaultProjectLimitSettings()), "Int32 D3999 must be valid");
  376. project.hmiPages.front().controls.back().binding =
  377. RegisterAddress{RegisterArea::D, 4000};
  378. require(!project.validate(defaultProjectLimitSettings()), "Int32 D4000 must be rejected");
  379. project = makeValidProject();
  380. HmiControl double_display = display;
  381. double_display.id = "double-display";
  382. double_display.dataType = RegisterDataType::Float64;
  383. double_display.binding = RegisterAddress{RegisterArea::D, 3996};
  384. project.hmiPages.front().controls.push_back(double_display);
  385. require(project.validate(defaultProjectLimitSettings()), "Double D3996 must be a valid even start address");
  386. project.hmiPages.front().controls.back().binding =
  387. RegisterAddress{RegisterArea::D, 3997};
  388. require(!project.validate(defaultProjectLimitSettings()), "Double D3997 must be rejected");
  389. project.hmiPages.front().controls.back().binding =
  390. RegisterAddress{RegisterArea::D, 3995};
  391. require(!project.validate(defaultProjectLimitSettings()), "Double odd start addresses must be rejected");
  392. project = makeValidProject();
  393. HmiControl invalid_display = display;
  394. invalid_display.id = "invalid-type-display";
  395. invalid_display.binding.reset();
  396. invalid_display.dataType = static_cast<RegisterDataType>(99);
  397. project.hmiPages.front().controls.push_back(invalid_display);
  398. require(!project.validate(defaultProjectLimitSettings()),
  399. "an unbound HMI draft must still reject an unknown numeric type");
  400. project = makeValidProject();
  401. HmiControl first = display;
  402. first.binding = RegisterAddress{RegisterArea::D, 10};
  403. HmiControl second = first;
  404. second.id = "float-display-duplicate";
  405. project.hmiPages.front().controls.push_back(first);
  406. project.hmiPages.front().controls.push_back(second);
  407. require(project.validate(defaultProjectLimitSettings()),
  408. "same Float32 start address and type may be bound more than once");
  409. second.dataType = RegisterDataType::Int16;
  410. project.hmiPages.front().controls.back() = second;
  411. require(!project.validate(defaultProjectLimitSettings()),
  412. "different HMI data types may not partially overlap");
  413. for (const RegisterDataType type : {
  414. RegisterDataType::Int32,
  415. RegisterDataType::Float32,
  416. RegisterDataType::Float64})
  417. {
  418. project = makeValidProject();
  419. HmiControl configured_multi_word = display;
  420. configured_multi_word.id = "protected-multi-word";
  421. configured_multi_word.dataType = type;
  422. configured_multi_word.binding = RegisterAddress{RegisterArea::D, 10};
  423. project.hmiPages.front().controls.push_back(configured_multi_word);
  424. project.controlLogics.front().rungs.front().output = LogicNode{
  425. "move-output",
  426. MoveNodeConfig{
  427. WordOperand{
  428. WordOperandKind::Constant,
  429. RegisterAddress{RegisterArea::D, 0},
  430. 1},
  431. RegisterAddress{
  432. RegisterArea::D,
  433. 10 + registerDataTypeWordCount(type) - 1}},
  434. true};
  435. require(!project.validate(defaultProjectLimitSettings()),
  436. "16-bit instructions must not write inside any multi-word HMI range");
  437. }
  438. }
  439. void testHmiAppearancePropertyBoundaries()
  440. {
  441. // 外观属性必须在领域层拒绝格式错误,但不能影响未知扩展属性
  442. Project project = makeValidProject();
  443. HmiControl &button = project.hmiPages.front().controls.front();
  444. button.properties[HmiAppearanceProperty::kTextColor] = "#E53935";
  445. button.properties[HmiAppearanceProperty::kFontSize] = "18";
  446. button.properties[HmiAppearanceProperty::kFontBold] = "true";
  447. button.properties[HmiAppearanceProperty::kFontItalic] = "false";
  448. require(project.validate(defaultProjectLimitSettings()), "valid HMI appearance properties must pass validation");
  449. button.properties[HmiAppearanceProperty::kTextColor] = "red";
  450. require(!project.validate(defaultProjectLimitSettings()), "text colors must use the #RRGGBB format");
  451. project = makeValidProject();
  452. HmiControl &font_control = project.hmiPages.front().controls.front();
  453. font_control.properties[HmiAppearanceProperty::kFontSize] = "5";
  454. require(!project.validate(defaultProjectLimitSettings()), "font sizes below the minimum must be rejected");
  455. font_control.properties[HmiAppearanceProperty::kFontSize] = "73";
  456. require(!project.validate(defaultProjectLimitSettings()), "font sizes above the maximum must be rejected");
  457. font_control.properties[HmiAppearanceProperty::kFontSize] = "large";
  458. require(!project.validate(defaultProjectLimitSettings()), "non-numeric font sizes must be rejected");
  459. project = makeValidProject();
  460. HmiControl &style_control = project.hmiPages.front().controls.front();
  461. style_control.properties[HmiAppearanceProperty::kFontBold] = "yes";
  462. require(!project.validate(defaultProjectLimitSettings()), "font style flags must be true or false");
  463. project = makeValidProject();
  464. project.hmiPages.front().controls.front().properties["legacyColor"] = "green";
  465. require(project.validate(defaultProjectLimitSettings()), "unknown HMI extension properties must remain supported");
  466. }
  467. void testButtonEnableConditionBoundaries()
  468. {
  469. Project project = makeValidProject();
  470. HmiControl &button = project.hmiPages.front().controls.front();
  471. button.buttonEnableCondition = HmiButtonBitEnableCondition{
  472. RegisterAddress{RegisterArea::M, 5}, false};
  473. require(project.validate(defaultProjectLimitSettings()),
  474. "valid M button enable conditions must pass validation");
  475. button.buttonEnableCondition = HmiButtonWordEnableCondition{
  476. RegisterAddress{RegisterArea::D, 20},
  477. RegisterDataType::Float64,
  478. HmiButtonConditionOperator::LessThan,
  479. 12.5};
  480. require(project.validate(defaultProjectLimitSettings()),
  481. "valid D button enable conditions must pass validation");
  482. button.buttonEnableCondition = HmiButtonBitEnableCondition{
  483. RegisterAddress{RegisterArea::D, 5}, true};
  484. require(!project.validate(defaultProjectLimitSettings()),
  485. "M button conditions must reject D addresses");
  486. project = makeValidProject();
  487. HmiControl &d_button = project.hmiPages.front().controls.front();
  488. d_button.buttonEnableCondition = HmiButtonWordEnableCondition{
  489. RegisterAddress{RegisterArea::D, 20},
  490. RegisterDataType::Int16,
  491. HmiButtonConditionOperator::Equal,
  492. 1.5};
  493. require(!project.validate(defaultProjectLimitSettings()),
  494. "Int16 button conditions must reject fractional comparison values");
  495. d_button.buttonEnableCondition = HmiButtonWordEnableCondition{
  496. RegisterAddress{RegisterArea::D, 3997},
  497. RegisterDataType::Float64,
  498. HmiButtonConditionOperator::Equal,
  499. 1.0};
  500. require(!project.validate(defaultProjectLimitSettings()),
  501. "Float64 button conditions must reject overflowing start addresses");
  502. d_button.buttonEnableCondition = HmiButtonWordEnableCondition{
  503. RegisterAddress{RegisterArea::D, 20},
  504. RegisterDataType::Int16,
  505. static_cast<HmiButtonConditionOperator>(99),
  506. 1.0};
  507. require(!project.validate(defaultProjectLimitSettings()),
  508. "button conditions must reject unknown comparison operators");
  509. HmiControl label = d_button;
  510. label.type = HmiControlType::Label;
  511. label.binding.reset();
  512. require(!label.validate(),
  513. "non-button controls must reject button enable conditions");
  514. }
  515. Project makeValidProject()
  516. {
  517. // 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线
  518. HmiControl start_button;
  519. start_button.id = "start-button";
  520. start_button.type = HmiControlType::Button;
  521. start_button.text = "Start";
  522. start_button.binding = {RegisterArea::M, 0};
  523. HmiPage page;
  524. page.id = "main-page";
  525. page.name = "Main";
  526. page.controls.push_back(start_button);
  527. LogicNode contact;
  528. contact.id = "start-contact";
  529. contact.config = ContactNodeConfig{
  530. RegisterAddress{RegisterArea::M, 0},
  531. ContactMode::NormallyOpen};
  532. LogicNode coil;
  533. coil.id = "run-coil";
  534. coil.config = CoilNodeConfig{
  535. RegisterAddress{RegisterArea::M, 1},
  536. CoilMode::Normal};
  537. ControlLogic logic;
  538. logic.id = "start-logic";
  539. logic.name = "Start logic";
  540. LadderRung rung;
  541. rung.id = "rung-1";
  542. rung.name = "Network 1";
  543. for (int column = 0;
  544. column < ProjectLimits::kMaximumConditionColumns;
  545. ++column)
  546. {
  547. rung.cells.push_back({
  548. "start-cell-" + std::to_string(column),
  549. column == 0 ? LadderCellKind::Node : LadderCellKind::Wire,
  550. column == 0 ? std::optional<LogicNode>{contact} : std::nullopt});
  551. }
  552. rung.output = coil;
  553. logic.rungs.push_back(rung);
  554. Project project;
  555. project.metadata = {"sample-project", "Sample project", "4.0"};
  556. project.hmiPages.push_back(page);
  557. project.initialHmiPageId = page.id;
  558. project.controlLogics.push_back(logic);
  559. return project;
  560. }
  561. void testMultiPageAndLogicDomainRules()
  562. {
  563. Project project = makeValidProject();
  564. HmiPage settings;
  565. settings.id = "settings-page";
  566. settings.name = "Settings";
  567. project.hmiPages.push_back(settings);
  568. HmiControl label;
  569. label.id = "title";
  570. label.type = HmiControlType::Label;
  571. label.text = "Machine";
  572. project.hmiPages.front().controls.push_back(label);
  573. require(project.validate(defaultProjectLimitSettings()), "an unbound label must be a valid static control");
  574. project.hmiPages.front().controls.back().binding =
  575. RegisterAddress{RegisterArea::M, 10};
  576. require(!project.validate(defaultProjectLimitSettings()), "labels must reject register bindings");
  577. project = makeValidProject();
  578. project.hmiPages.push_back(settings);
  579. HmiControl jump;
  580. jump.id = "settings-jump";
  581. jump.type = HmiControlType::PageJump;
  582. jump.text = "Settings";
  583. jump.pageJump = HmiPageJumpConfig{settings.id};
  584. project.hmiPages.front().controls.push_back(jump);
  585. require(project.validate(defaultProjectLimitSettings()) && project.validateForRunning(defaultProjectLimitSettings()),
  586. "a page jump must resolve its target by stable page id");
  587. project.hmiPages.front().controls.back().pageJump->targetPageId = "missing";
  588. require(!project.validate(defaultProjectLimitSettings()), "a page jump must reject a missing target page");
  589. project = makeValidProject();
  590. project.initialHmiPageId = "missing";
  591. require(!project.validate(defaultProjectLimitSettings()), "the initial HMI page id must resolve to a page");
  592. project = makeValidProject();
  593. HmiPage duplicate_name = settings;
  594. duplicate_name.name = project.hmiPages.front().name;
  595. project.hmiPages.push_back(duplicate_name);
  596. require(!project.validate(defaultProjectLimitSettings()), "HMI page names must be unique");
  597. project = makeValidProject();
  598. ControlLogic disabled_draft;
  599. disabled_draft.id = "draft-logic";
  600. disabled_draft.name = "Draft logic";
  601. disabled_draft.enabled = false;
  602. LadderRung draft_rung;
  603. draft_rung.id = "rung-1";
  604. draft_rung.name = "Draft network";
  605. disabled_draft.rungs.push_back(std::move(draft_rung));
  606. project.controlLogics.push_back(disabled_draft);
  607. require(project.validateForRunning(defaultProjectLimitSettings()),
  608. "a disabled draft logic must not block offline running");
  609. project.controlLogics.back().name = project.controlLogics.front().name;
  610. require(!project.validate(defaultProjectLimitSettings()), "control logic names must be unique");
  611. project = makeValidProject();
  612. project.hmiPages.front().controls.front().type =
  613. static_cast<HmiControlType>(99);
  614. project.hmiPages.front().controls.front().binding.reset();
  615. require(!project.validate(defaultProjectLimitSettings()), "unknown HMI control types must be rejected");
  616. }
  617. void testQuantityBoundaries()
  618. {
  619. Project project = makeValidProject();
  620. for (std::size_t index = 1U; index < ProjectLimits::kMaximumHmiPages; ++index)
  621. {
  622. project.hmiPages.push_back({
  623. "page-" + std::to_string(index),
  624. "Page " + std::to_string(index),
  625. 800,
  626. 400,
  627. {}});
  628. }
  629. require(project.validate(defaultProjectLimitSettings()), "an HMI page count at the configured limit must be accepted");
  630. project.hmiPages.push_back({"page-over", "Page over", 800, 400, {}});
  631. require(
  632. !project.validate(defaultProjectLimitSettings()),
  633. "an HMI page count of "
  634. + std::to_string(ProjectLimits::kMaximumHmiPages + 1U)
  635. + " must be rejected");
  636. project = makeValidProject();
  637. project.hmiPages.clear();
  638. project.initialHmiPageId.clear();
  639. for (std::size_t page_index = 0U; page_index < 17U; ++page_index)
  640. {
  641. HmiPage page{
  642. "bulk-page-" + std::to_string(page_index),
  643. "Bulk page " + std::to_string(page_index),
  644. 800,
  645. 400,
  646. {}};
  647. for (std::size_t control_index = 0U;
  648. control_index < ProjectLimits::kMaximumHmiControlsPerPage;
  649. ++control_index)
  650. {
  651. HmiControl label;
  652. label.id = "label-" + std::to_string(control_index);
  653. label.type = HmiControlType::Label;
  654. label.bounds = {0, 0, 1, 1};
  655. label.text = "label";
  656. page.controls.push_back(std::move(label));
  657. }
  658. project.hmiPages.push_back(std::move(page));
  659. }
  660. project.initialHmiPageId = project.hmiPages.front().id;
  661. require(!project.validate(defaultProjectLimitSettings()),
  662. "an HMI control count over the project limit must be rejected");
  663. project = makeValidProject();
  664. project.hmiPages.front().controls.clear();
  665. for (std::size_t index = 0U;
  666. index < ProjectLimits::kMaximumHmiControlsPerPage;
  667. ++index)
  668. {
  669. HmiControl label;
  670. label.id = "label-" + std::to_string(index);
  671. label.type = HmiControlType::Label;
  672. label.bounds = {0, 0, 1, 1};
  673. label.text = "label";
  674. project.hmiPages.front().controls.push_back(std::move(label));
  675. }
  676. require(project.validate(defaultProjectLimitSettings()), "a page control count at the configured limit must be accepted");
  677. HmiControl extra_label;
  678. extra_label.id = "label-over";
  679. extra_label.type = HmiControlType::Label;
  680. extra_label.bounds = {0, 0, 1, 1};
  681. extra_label.text = "label";
  682. project.hmiPages.front().controls.push_back(std::move(extra_label));
  683. require(
  684. !project.validate(defaultProjectLimitSettings()),
  685. "a page control count of "
  686. + std::to_string(ProjectLimits::kMaximumHmiControlsPerPage + 1U)
  687. + " must be rejected");
  688. project = makeValidProject();
  689. project.controlLogics.clear();
  690. for (std::size_t logic_index = 0U; logic_index < 9U; ++logic_index)
  691. {
  692. ControlLogic logic{
  693. "bulk-logic-" + std::to_string(logic_index),
  694. "Bulk logic " + std::to_string(logic_index),
  695. {},
  696. true,
  697. {}};
  698. for (std::size_t rung_index = 0U;
  699. rung_index < ProjectLimits::kMaximumRungsPerLogic;
  700. ++rung_index)
  701. {
  702. logic.rungs.push_back({
  703. "rung-" + std::to_string(rung_index),
  704. "Rung " + std::to_string(rung_index),
  705. {},
  706. std::nullopt,
  707. {}});
  708. }
  709. project.controlLogics.push_back(std::move(logic));
  710. }
  711. require(!project.validate(defaultProjectLimitSettings()),
  712. "a ladder rung count over the project limit must be rejected");
  713. project = makeValidProject();
  714. project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth;
  715. project.hmiPages.front().height = ProjectLimits::kMaximumHmiPageHeight;
  716. require(project.validate(defaultProjectLimitSettings()), "an HMI page size of 1600 by 800 must be accepted");
  717. project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth + 1;
  718. require(!project.validate(defaultProjectLimitSettings()), "an HMI page width of 1601 must be rejected");
  719. project.hmiPages.front().width = ProjectLimits::kMinimumHmiPageWidth - 1;
  720. require(!project.validate(defaultProjectLimitSettings()), "an HMI page width of 319 must be rejected");
  721. project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth;
  722. project.hmiPages.front().height = ProjectLimits::kMinimumHmiPageHeight - 1;
  723. require(!project.validate(defaultProjectLimitSettings()), "an HMI page height of 199 must be rejected");
  724. project = makeValidProject();
  725. project.controlLogics.front().rungs.front().cells.pop_back();
  726. require(!project.validate(defaultProjectLimitSettings()),
  727. "a ladder row with fewer than ten cells must be rejected");
  728. }
  729. void testLogicNodeConfigurationBoundaries()
  730. {
  731. // 触点只能绑定 M 区,数值比较只能绑定 D 区
  732. LogicNode contact;
  733. contact.id = "contact";
  734. contact.config = ContactNodeConfig{
  735. RegisterAddress{RegisterArea::M, 0},
  736. ContactMode::NormallyOpen};
  737. require(contact.validate(), "contact node bound to M address must be valid");
  738. contact.config = ContactNodeConfig{
  739. RegisterAddress{RegisterArea::D, 0},
  740. ContactMode::NormallyOpen};
  741. require(!contact.validate(), "contact node bound to D address must be rejected");
  742. LogicNode comparison;
  743. comparison.id = "comparison";
  744. comparison.config = CompareNodeConfig{
  745. RegisterAddress{RegisterArea::D, 0},
  746. ComparisonOperator::GreaterThan,
  747. static_cast<std::int16_t>(100)};
  748. require(comparison.validate(), "comparison node bound to D address must be valid");
  749. }
  750. void testEdgeAndCommentBoundaries()
  751. {
  752. LogicNode edge;
  753. edge.id = "edge";
  754. edge.config = EdgeContactNodeConfig{
  755. RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising};
  756. require(edge.validate(), "a valid rising edge contact must pass validation");
  757. RegisterComment comment{RegisterAddress{RegisterArea::M, 0}, "启动按钮"};
  758. require(comment.validate(), "a nonblank register comment must be valid");
  759. comment.text.assign(ProjectLimits::kMaximumRegisterCommentBytes, 'a');
  760. require(comment.validate(), "a register comment at the byte limit must be valid");
  761. comment.text.push_back('a');
  762. require(!comment.validate(), "a register comment above the byte limit must fail");
  763. comment.text = "启动\n按钮";
  764. require(!comment.validate(), "a multiline register comment must be rejected");
  765. comment.text = "启动\r按钮";
  766. require(!comment.validate(), "a register comment containing CR must be rejected");
  767. comment.text = " \t";
  768. require(!comment.validate(), "a blank register comment must be rejected");
  769. LadderRung comment_rung;
  770. comment_rung.id = "comment-rung";
  771. comment_rung.name = "Comment rung";
  772. comment_rung.comment.assign(ProjectLimits::kMaximumRungCommentBytes, 'a');
  773. require(comment_rung.validateStructure(), "a rung comment at the byte limit must be valid");
  774. comment_rung.comment.push_back('a');
  775. require(!comment_rung.validateStructure(), "a rung comment above the byte limit must fail");
  776. comment_rung.comment = "第一行\n第二行";
  777. require(!comment_rung.validateStructure(), "a multiline rung comment must be rejected");
  778. comment_rung.comment = "第一行\r第二行";
  779. require(!comment_rung.validateStructure(), "a rung comment containing CR must be rejected");
  780. ControlLogic commented_logic;
  781. commented_logic.id = "commented-logic";
  782. commented_logic.name = "Commented logic";
  783. LadderRung head;
  784. head.id = "head";
  785. head.name = "Head";
  786. head.comment = "Network comment";
  787. LadderRung branch;
  788. branch.id = "branch";
  789. branch.name = "Branch";
  790. branch.comment = "Hidden branch comment";
  791. commented_logic.rungs = {head, branch};
  792. commented_logic.verticalConnections = {
  793. {"comment-edge", "head", "branch", 0}};
  794. require(
  795. commented_logic.networkHeadIndex(1U) == 0U
  796. && !commented_logic.validateStructure(),
  797. "a connected branch row must not persist a hidden network comment");
  798. commented_logic.rungs[1].comment.clear();
  799. require(commented_logic.validateStructure(),
  800. "a connected network must be valid when only its head stores the comment");
  801. Project project = makeValidProject();
  802. project.registerComments = {
  803. {RegisterAddress{RegisterArea::M, 0}, "启动按钮"},
  804. {RegisterAddress{RegisterArea::M, 0}, "重复地址"}};
  805. require(!project.validate(defaultProjectLimitSettings()), "duplicate register comments must be rejected");
  806. }
  807. void testDataInstructionBoundaries()
  808. {
  809. LogicNode move;
  810. move.id = "move";
  811. move.config = MoveNodeConfig{
  812. WordOperand{
  813. WordOperandKind::Constant,
  814. RegisterAddress{RegisterArea::D, 0},
  815. -100},
  816. RegisterAddress{RegisterArea::D, 20}};
  817. require(move.validate() && move.isOutput(),
  818. "MOVE with a constant source and D destination must be a valid output");
  819. LogicNode add;
  820. add.id = "add";
  821. add.config = ArithmeticNodeConfig{
  822. ArithmeticOperation::Add,
  823. WordOperand{
  824. WordOperandKind::Register,
  825. RegisterAddress{RegisterArea::D, 20},
  826. 0},
  827. WordOperand{
  828. WordOperandKind::Constant,
  829. RegisterAddress{RegisterArea::D, 0},
  830. 1},
  831. RegisterAddress{RegisterArea::D, 20}};
  832. require(add.validate() && add.isOutput(),
  833. "ADD must allow the same D register as source and destination");
  834. }
  835. void testLadderLogicBoundaries()
  836. {
  837. ControlLogic logic;
  838. logic.id = "grid-logic";
  839. logic.name = "Grid logic";
  840. LadderRung upper;
  841. upper.id = "rung-1";
  842. upper.name = "Row 1";
  843. LadderRung lower;
  844. lower.id = "rung-2";
  845. lower.name = "Row 2";
  846. for (int column = 0;
  847. column < ProjectLimits::kMaximumConditionColumns;
  848. ++column)
  849. {
  850. upper.cells.push_back({
  851. "upper-cell-" + std::to_string(column),
  852. LadderCellKind::Wire,
  853. std::nullopt});
  854. lower.cells.push_back({
  855. "lower-cell-" + std::to_string(column),
  856. LadderCellKind::Gap,
  857. std::nullopt});
  858. }
  859. upper.cells[0].kind = LadderCellKind::Node;
  860. upper.cells[0].node = LogicNode{
  861. "start",
  862. ContactNodeConfig{
  863. RegisterAddress{RegisterArea::M, 0},
  864. ContactMode::NormallyOpen},
  865. true};
  866. upper.output = LogicNode{
  867. "run-coil",
  868. CoilNodeConfig{
  869. RegisterAddress{RegisterArea::M, 1}, CoilMode::Normal},
  870. true};
  871. logic.rungs = {upper, lower};
  872. logic.verticalConnections = {
  873. {"vertical-left", "rung-1", "rung-2", 0},
  874. {"vertical-right", "rung-1", "rung-2", 1}};
  875. require(logic.validate(defaultProjectLimitSettings()) && logic.validateForRunning(defaultProjectLimitSettings()),
  876. "a ten-cell grid with adjacent vertical edges must be valid");
  877. logic.rungs.front().cells[5].kind = LadderCellKind::Gap;
  878. std::string connectivity_error;
  879. require(
  880. logic.validate(defaultProjectLimitSettings()) && !logic.validateForRunning(&connectivity_error)
  881. && connectivity_error.find("第 1 行") != std::string::npos
  882. && connectivity_error.find("第 6 列") != std::string::npos,
  883. "a disconnected output must report its visual row and break column");
  884. logic.rungs.front() = upper;
  885. logic.rungs.front().cells[5].kind = LadderCellKind::Gap;
  886. for (LadderCell &cell : logic.rungs.back().cells)
  887. {
  888. cell.kind = LadderCellKind::Wire;
  889. cell.node.reset();
  890. }
  891. logic.verticalConnections = {
  892. {"vertical-left", "rung-1", "rung-2", 0},
  893. {"vertical-bypass", "rung-1", "rung-2", 6}};
  894. require(
  895. logic.validateForRunning(defaultProjectLimitSettings()),
  896. "a vertical branch that bypasses a gap must keep the output reachable");
  897. logic.rungs = {upper, lower};
  898. logic.verticalConnections = {
  899. {"vertical-left", "rung-1", "rung-2", 0},
  900. {"vertical-right", "rung-1", "rung-2", 1}};
  901. logic.rungs.front().cells.front().node = LogicNode{
  902. "invalid-coil",
  903. CoilNodeConfig{
  904. RegisterAddress{RegisterArea::M, 2}, CoilMode::Normal},
  905. true};
  906. require(!logic.validate(defaultProjectLimitSettings()), "a condition cell must reject output nodes");
  907. logic.rungs.front() = upper;
  908. logic.rungs.front().output = LogicNode{
  909. "invalid-contact",
  910. ContactNodeConfig{
  911. RegisterAddress{RegisterArea::M, 2}, ContactMode::NormallyOpen},
  912. true};
  913. require(!logic.validate(defaultProjectLimitSettings()), "the output slot must reject condition nodes");
  914. logic.rungs.front() = upper;
  915. logic.rungs.front().output.reset();
  916. require(logic.validate(defaultProjectLimitSettings()) && logic.validateForRunning(defaultProjectLimitSettings()),
  917. "a row without an output may act as a connected branch");
  918. logic.rungs.front() = upper;
  919. logic.rungs.front().cells[1].id = logic.rungs.front().cells[0].id;
  920. require(!logic.validate(defaultProjectLimitSettings()), "cell ids must be unique within a logic");
  921. logic.rungs.front() = upper;
  922. logic.verticalConnections.front().lowerRungId = "missing-rung";
  923. require(!logic.validate(defaultProjectLimitSettings()), "vertical edges must reference adjacent rows");
  924. logic.verticalConnections = {
  925. {"vertical-left", "rung-1", "rung-2", 0},
  926. {"vertical-copy", "rung-1", "rung-2", 0}};
  927. require(!logic.validate(defaultProjectLimitSettings()),
  928. "one row boundary must not contain duplicate vertical edges");
  929. }
  930. void testModelsValidateBindingsAndIdentifiers()
  931. {
  932. // 聚合验证必须拒绝错误绑定、重复标识和越界控件
  933. Project project = makeValidProject();
  934. require(project.validate(defaultProjectLimitSettings()), "valid project model must pass validation");
  935. project.hmiPages.front().controls.front().binding =
  936. RegisterAddress{RegisterArea::D, 0};
  937. require(!project.validate(defaultProjectLimitSettings()), "button bound to D area must be rejected");
  938. project = makeValidProject();
  939. project.hmiPages.push_back(project.hmiPages.front());
  940. require(!project.validate(defaultProjectLimitSettings()), "duplicate HMI page id must be rejected");
  941. project = makeValidProject();
  942. project.hmiPages.front().controls.front().bounds.x = -1;
  943. require(!project.validate(defaultProjectLimitSettings()), "controls outside the page must be rejected");
  944. project = makeValidProject();
  945. project.hmiPages.front().controls.front().bounds.width = 801;
  946. require(!project.validate(defaultProjectLimitSettings()), "controls wider than the page must be rejected");
  947. project = makeValidProject();
  948. project.hmiPages.front().controls.front().properties.emplace("", "value");
  949. require(!project.validate(defaultProjectLimitSettings()), "empty HMI property names must be rejected");
  950. project = makeValidProject();
  951. project.hmiPages.front().controls.front().binding.reset();
  952. require(project.validate(defaultProjectLimitSettings()), "unbound HMI control must be accepted in a draft");
  953. require(!project.validateForRunning(defaultProjectLimitSettings()),
  954. "unbound HMI control must block runtime validation");
  955. project = makeValidProject();
  956. project.controlLogics.front().rungs.front().output->configured = false;
  957. require(project.validate(defaultProjectLimitSettings()), "unconfigured ladder node must be accepted in a draft");
  958. require(!project.validateForRunning(defaultProjectLimitSettings()),
  959. "unconfigured ladder node must block runtime validation");
  960. }
  961. void testRuntimeStateBoundaries()
  962. {
  963. // 运行模式测试覆盖离线和真机的互斥及 PLC 首读前置条件
  964. RuntimeState state;
  965. require(state.policy().allowsProjectEditing, "editing mode must allow project editing");
  966. require(state.enterOfflineRunning().succeeded, "editing may enter offline running");
  967. require(state.policy().usesVirtualRegisters, "offline mode must use virtual registers");
  968. require(state.policy().runsLogicExecutor, "offline mode must run logic executor");
  969. require(state.enterOnlineRunning(true).error
  970. == ModeTransitionError::MustReturnToEditing,
  971. "offline mode must not directly enter online mode");
  972. require(state.enterEditing().succeeded, "offline mode may return to editing");
  973. require(state.enterOnlineRunning(false).error
  974. == ModeTransitionError::InitialPlcReadRequired,
  975. "online mode must require an initial PLC read");
  976. require(state.enterOnlineRunning(true).succeeded,
  977. "editing may enter online mode after initial PLC read");
  978. require(state.policy().runsLogicExecutor,
  979. "online mode must run the local read-only trace executor");
  980. require(state.policy().usesPlcRegisters, "online mode must use PLC registers");
  981. }
  982. void testRuntimeConfiguredProjectLimits()
  983. {
  984. ProjectLimitSettings limits;
  985. std::string error;
  986. Project project = makeValidProject();
  987. HmiPage second_page = project.hmiPages.front();
  988. second_page.id = "second-page";
  989. second_page.name = "Second page";
  990. project.hmiPages.push_back(second_page);
  991. limits.maximumHmiPages = 1U;
  992. require(!project.validate(limits, &error)
  993. && error.find("当前配置上限为 1") != std::string::npos,
  994. "runtime page limits must be enforced by aggregate validation");
  995. project = makeValidProject();
  996. HmiControl second_control = project.hmiPages.front().controls.front();
  997. second_control.id = "second-control";
  998. project.hmiPages.front().controls.push_back(second_control);
  999. limits = {};
  1000. limits.maximumHmiControlsPerPage = 1U;
  1001. require(!project.validate(limits, &error),
  1002. "runtime per-page control limits must be enforced");
  1003. project = makeValidProject();
  1004. project.alarmDefinitions.push_back({});
  1005. limits = {};
  1006. limits.maximumAlarmDefinitions = 0U;
  1007. require(!project.validate(limits, &error),
  1008. "runtime alarm limits must be enforced before child validation");
  1009. project = makeValidProject();
  1010. ControlLogic second_logic = project.controlLogics.front();
  1011. second_logic.id = "second-logic";
  1012. second_logic.name = "Second logic";
  1013. second_logic.rungs.clear();
  1014. project.controlLogics.push_back(second_logic);
  1015. limits = {};
  1016. limits.maximumControlLogics = 1U;
  1017. require(!project.validate(limits, &error),
  1018. "runtime control-logic limits must be enforced");
  1019. project = makeValidProject();
  1020. limits = {};
  1021. limits.maximumRungsPerLogic = 0U;
  1022. require(!project.validate(limits, &error),
  1023. "runtime per-logic rung limits must be enforced");
  1024. }
  1025. } // namespace
  1026. int main()
  1027. {
  1028. return TestSupport::runTestSuite("domain tests", {
  1029. {"testRegisterAddressBoundaries", testRegisterAddressBoundaries},
  1030. {"testRegisterAddressParsing", testRegisterAddressParsing},
  1031. {"testRegisterRepositorySeparatesAreas", testRegisterRepositorySeparatesAreas},
  1032. {"testMultiWordCodecsAndBlockAccess", testMultiWordCodecsAndBlockAccess},
  1033. {"testHmiControlRegistryCompleteness", testHmiControlRegistryCompleteness},
  1034. {"testStatusTextDomainRules", testStatusTextDomainRules},
  1035. {"testMultiWordHmiBoundaries", testMultiWordHmiBoundaries},
  1036. {"testHmiAppearancePropertyBoundaries", testHmiAppearancePropertyBoundaries},
  1037. {"testButtonEnableConditionBoundaries", testButtonEnableConditionBoundaries},
  1038. {"testLogicNodeConfigurationBoundaries", testLogicNodeConfigurationBoundaries},
  1039. {"testEdgeAndCommentBoundaries", testEdgeAndCommentBoundaries},
  1040. {"testDataInstructionBoundaries", testDataInstructionBoundaries},
  1041. {"testLadderLogicBoundaries", testLadderLogicBoundaries},
  1042. {"testModelsValidateBindingsAndIdentifiers", testModelsValidateBindingsAndIdentifiers},
  1043. {"testMultiPageAndLogicDomainRules", testMultiPageAndLogicDomainRules},
  1044. {"testQuantityBoundaries", testQuantityBoundaries},
  1045. {"testRuntimeConfiguredProjectLimits", testRuntimeConfiguredProjectLimits},
  1046. {"testRuntimeStateBoundaries", testRuntimeStateBoundaries},
  1047. });
  1048. }