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

502 satır
24 KiB

  1. #include "domain/project_storage.h"
  2. #include "domain/register_repository.h"
  3. #include "domain/virtual_register_repository.h"
  4. #include "services/hmi_editor_service.h"
  5. #include "services/editor_history.h"
  6. #include "services/hmi_runtime_service.h"
  7. #include "services/hmi_navigation_service.h"
  8. #include "services/project_service.h"
  9. #include "support/test_support.h"
  10. #include <exception>
  11. #include <iostream>
  12. #include <stdexcept>
  13. #include <string>
  14. namespace {
  15. using TestProjectStorage = TestSupport::InMemoryProjectStorage;
  16. using TestSupport::require;
  17. void testControlEditing()
  18. {
  19. // 覆盖控件创建、移动、绑定校验、重命名冲突和删除流程
  20. TestProjectStorage storage;
  21. ProjectService project_service(storage);
  22. HmiEditorService service(project_service);
  23. const HmiEditorResult page_result = service.ensureDefaultPage();
  24. require(page_result.succeeded, "default HMI page creation must succeed");
  25. const std::string page_id = page_result.id;
  26. const HmiEditorResult button = service.addControl(page_id, HmiControlType::Button);
  27. const HmiEditorResult indicator = service.addControl(page_id, HmiControlType::Indicator);
  28. const HmiEditorResult display = service.addControl(
  29. page_id, HmiControlType::NumericDisplay);
  30. const HmiEditorResult input = service.addControl(page_id, HmiControlType::NumericInput);
  31. require(button.succeeded && indicator.succeeded && display.succeeded
  32. && input.succeeded,
  33. "basic HMI controls must be added");
  34. const HmiPage *page = service.findPage(page_id);
  35. require(page != nullptr && page->controls.size() == 4,
  36. "all added controls must be kept in the page model");
  37. require(service.moveControl(page_id, button.id, {120, 80, 120, 40}).succeeded,
  38. "a valid control move must succeed");
  39. require(!service.moveControl(page_id, button.id, {790, 460, 120, 40}).succeeded,
  40. "a move outside the page must be rejected");
  41. HmiControl updated = *service.findControl(page_id, button.id);
  42. updated.binding = RegisterAddress{RegisterArea::D, 0};
  43. require(!service.updateControl(page_id, button.id, updated).succeeded,
  44. "buttons must reject D address bindings during editing");
  45. updated.binding = RegisterAddress{RegisterArea::M, 7};
  46. updated.text = "启动";
  47. require(service.updateControl(page_id, button.id, updated).succeeded,
  48. "a button M binding and edited text must be accepted");
  49. HmiControl duplicate = *service.findControl(page_id, indicator.id);
  50. duplicate.id = button.id;
  51. require(service.updateControl(page_id, indicator.id, duplicate).error
  52. == HmiEditorError::DuplicateId,
  53. "duplicate control ids must be rejected");
  54. require(service.removeControl(page_id, indicator.id).succeeded,
  55. "deleting a selected control must succeed");
  56. require(service.findControl(page_id, indicator.id) == nullptr,
  57. "deleted controls must not remain in the model");
  58. }
  59. void testRuntimeUsesRegisterRepository()
  60. {
  61. // 运行服务只能经由仓库接口读写 M/D,不依赖具体离线实现
  62. VirtualRegisterRepository repository;
  63. HmiRuntimeService runtime_service(repository);
  64. HmiControl button;
  65. button.id = "start";
  66. button.type = HmiControlType::Button;
  67. button.binding = RegisterAddress{RegisterArea::M, 12};
  68. require(button.buttonOperation == HmiButtonOperation::MomentaryOn,
  69. "new buttons must default to momentary ON");
  70. require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded,
  71. "momentary button press must be accepted by the repository");
  72. require(repository.readBit(*button.binding).value,
  73. "momentary button press must write ON");
  74. require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded,
  75. "momentary button release must be accepted by the repository");
  76. require(!repository.readBit(*button.binding).value,
  77. "momentary button release must write OFF");
  78. button.buttonOperation = HmiButtonOperation::SetOn;
  79. require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded,
  80. "set ON button press must succeed");
  81. require(repository.readBit(*button.binding).value,
  82. "set ON button press must write ON");
  83. require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded,
  84. "set ON button release must be ignored successfully");
  85. require(repository.readBit(*button.binding).value,
  86. "set ON button release must keep the bit ON");
  87. button.buttonOperation = HmiButtonOperation::SetOff;
  88. require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded,
  89. "set OFF button press must succeed");
  90. require(!repository.readBit(*button.binding).value,
  91. "set OFF button press must write OFF");
  92. button.buttonOperation = HmiButtonOperation::Toggle;
  93. require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded,
  94. "toggle button press must succeed");
  95. require(repository.readBit(*button.binding).value,
  96. "toggle button press must invert the current value");
  97. HmiControl indicator;
  98. indicator.id = "running";
  99. indicator.type = HmiControlType::Indicator;
  100. indicator.binding = RegisterAddress{RegisterArea::M, 12};
  101. const HmiRuntimeReadResult indicator_value = runtime_service.readControl(indicator);
  102. require(indicator_value.succeeded && indicator_value.bit_value,
  103. "indicators must read M values through the repository");
  104. HmiControl numeric_input;
  105. numeric_input.id = "target";
  106. numeric_input.type = HmiControlType::NumericInput;
  107. numeric_input.binding = RegisterAddress{RegisterArea::D, 9};
  108. require(runtime_service.writeNumericInput(numeric_input, -18).succeeded,
  109. "numeric input runtime writes must be accepted by the repository");
  110. HmiControl numeric_display;
  111. numeric_display.id = "actual";
  112. numeric_display.type = HmiControlType::NumericDisplay;
  113. numeric_display.binding = RegisterAddress{RegisterArea::D, 9};
  114. const HmiRuntimeReadResult numeric_value = runtime_service.readControl(numeric_display);
  115. require(numeric_value.succeeded && numeric_value.word_value == -18,
  116. "numeric display must read D values through the repository");
  117. }
  118. void testHistoryAndAtomicBatchDelete()
  119. {
  120. TestProjectStorage storage;
  121. ProjectService project_service(storage);
  122. HmiEditorService service(project_service);
  123. const std::string page_id = service.ensureDefaultPage().id;
  124. const HmiEditorResult first = service.addControl(page_id, HmiControlType::Label);
  125. const HmiEditorResult second = service.addControl(page_id, HmiControlType::Label);
  126. const HmiEditorResult third = service.addControl(page_id, HmiControlType::Label);
  127. require(first.succeeded && second.succeeded && third.succeeded,
  128. "controls for history testing must be created");
  129. service.clearHistory();
  130. require(!service.removeControls(page_id, {first.id, "missing-control"}).succeeded,
  131. "batch deletion must reject an unknown control before changing the page");
  132. require(service.findPage(page_id)->controls.size() == 3U
  133. && !service.canUndo(),
  134. "failed batch deletion must be atomic and leave history unchanged");
  135. require(service.removeControls(page_id, {first.id, second.id}).succeeded,
  136. "batch deletion of valid controls must succeed");
  137. require(service.findPage(page_id)->controls.size() == 1U
  138. && service.canUndo(),
  139. "valid batch deletion must remove all selected controls in one history step");
  140. require(service.undo().succeeded && service.findPage(page_id)->controls.size() == 3U,
  141. "HMI undo must restore a deleted batch");
  142. require(service.redo().succeeded && service.findPage(page_id)->controls.size() == 1U,
  143. "HMI redo must reapply a deleted batch");
  144. service.clearHistory();
  145. const HmiControl *remaining = service.findControl(page_id, third.id);
  146. require(remaining != nullptr,
  147. "the unselected control must survive a batch deletion");
  148. require(service.moveControl(page_id, third.id, remaining->bounds).succeeded
  149. && !service.canUndo(),
  150. "a no-op control move must not consume an undo step");
  151. service.clearHistory();
  152. HmiControl candidate = *service.findControl(page_id, third.id);
  153. for (int index = 1; index <= 101; ++index)
  154. {
  155. candidate.bounds.x = index;
  156. require(service.updateControl(page_id, third.id, candidate).succeeded,
  157. "repeated valid HMI edits must succeed");
  158. }
  159. int undo_count = 0;
  160. while (service.undo().succeeded)
  161. {
  162. ++undo_count;
  163. }
  164. require(undo_count == static_cast<int>(EditorHistory<int>::kMaximumEntries),
  165. "HMI history must retain exactly the configured most recent steps");
  166. }
  167. void testBatchPasteControls()
  168. {
  169. TestProjectStorage storage;
  170. ProjectService project_service(storage);
  171. HmiEditorService service(project_service);
  172. const std::string page_id = service.ensureDefaultPage().id;
  173. const HmiEditorResult first = service.addControl(page_id, HmiControlType::Label);
  174. const HmiEditorResult second = service.addControl(page_id, HmiControlType::Label);
  175. require(first.succeeded && second.succeeded,
  176. "controls for paste testing must be created");
  177. HmiControl first_copy = *service.findControl(page_id, first.id);
  178. HmiControl second_copy = *service.findControl(page_id, second.id);
  179. first_copy.bounds = {100, 80, 80, 32};
  180. first_copy.text = "复制标签";
  181. first_copy.properties[HmiAppearanceProperty::kTextColor] = "#E53935";
  182. second_copy.bounds = {220, 80, 80, 32};
  183. require(service.updateControl(page_id, first.id, first_copy).succeeded
  184. && service.updateControl(page_id, second.id, second_copy).succeeded,
  185. "source controls must be movable before paste");
  186. service.clearHistory();
  187. const HmiEditorResult pasted = service.pasteControls(
  188. page_id, {first_copy, second_copy});
  189. require(pasted.succeeded
  190. && service.findPage(page_id)->controls.size() == 4U
  191. && pasted.id != first.id && pasted.id != second.id,
  192. "batch paste must create controls with fresh ids");
  193. const HmiControl *pasted_control = service.findControl(page_id, pasted.id);
  194. require(pasted_control != nullptr
  195. && pasted_control->bounds.x == first_copy.bounds.x + 20
  196. && pasted_control->bounds.y == first_copy.bounds.y + 20
  197. && pasted_control->text == first_copy.text
  198. && pasted_control->properties == first_copy.properties,
  199. "pasted controls must preserve content and appearance with an offset");
  200. require(service.undo().succeeded
  201. && service.findPage(page_id)->controls.size() == 2U,
  202. "batch control paste must be one undoable operation");
  203. }
  204. void testRuntimeValidationAndPasteBoundaries()
  205. {
  206. TestProjectStorage storage;
  207. ProjectService project_service(storage);
  208. HmiEditorService editor(project_service);
  209. const std::string page_id = editor.ensureDefaultPage().id;
  210. HmiControl label;
  211. label.id = "label-source";
  212. label.type = HmiControlType::Label;
  213. label.bounds = {760, 360, 32, 32};
  214. label.text = "边界";
  215. require(editor.pasteControls(page_id, {label}, 20, 20).succeeded,
  216. "a paste near the page edge must be clamped into the page");
  217. const HmiPage *page = editor.findPage(page_id);
  218. require(page != nullptr && page->controls.size() == 1U,
  219. "edge paste must add exactly one control");
  220. const HmiControl &edge_copy = page->controls.front();
  221. require(edge_copy.bounds.x + edge_copy.bounds.width <= page->width
  222. && edge_copy.bounds.y + edge_copy.bounds.height <= page->height
  223. && edge_copy.bounds.x >= 0 && edge_copy.bounds.y >= 0,
  224. "edge paste must keep the copied control inside page bounds");
  225. require(!editor.pasteControls(page_id, {}).succeeded,
  226. "an empty HMI clipboard must be rejected");
  227. HmiControl oversized = label;
  228. oversized.bounds = {0, 0, page->width + 1, 32};
  229. require(!editor.pasteControls(page_id, {oversized}).succeeded
  230. && editor.findPage(page_id)->controls.size() == 1U,
  231. "a copied control wider than the target page must fail atomically");
  232. HmiControl unsupported = label;
  233. unsupported.type = HmiControlType::Count;
  234. require(!editor.pasteControls(page_id, {unsupported}).succeeded
  235. && editor.findPage(page_id)->controls.size() == 1U,
  236. "an unsupported copied HMI type must fail atomically");
  237. std::vector<HmiControl> too_many(513U, label);
  238. require(!editor.pasteControls(page_id, too_many).succeeded
  239. && editor.findPage(page_id)->controls.size() == 1U,
  240. "a paste batch above the page limit must be rejected before mutation");
  241. VirtualRegisterRepository repository;
  242. HmiRuntimeService runtime(repository);
  243. HmiControl unbound_indicator;
  244. unbound_indicator.type = HmiControlType::Indicator;
  245. require(runtime.readControl(unbound_indicator).error
  246. == HmiRuntimeError::MissingBinding,
  247. "an unbound runtime indicator must report MissingBinding");
  248. HmiControl wrong_area_indicator = unbound_indicator;
  249. wrong_area_indicator.binding = RegisterAddress{RegisterArea::D, 0};
  250. require(runtime.readControl(wrong_area_indicator).error
  251. == HmiRuntimeError::InvalidBinding,
  252. "an indicator bound to D must report InvalidBinding");
  253. HmiControl invalid_indicator = unbound_indicator;
  254. invalid_indicator.binding = RegisterAddress{RegisterArea::M, 4001};
  255. require(runtime.readControl(invalid_indicator).error
  256. == HmiRuntimeError::InvalidBinding,
  257. "an indicator with an out-of-range address must report InvalidBinding");
  258. require(runtime.readControl(label).error
  259. == HmiRuntimeError::UnsupportedControl,
  260. "a static label must not be treated as a register runtime control");
  261. HmiControl wrong_button = unbound_indicator;
  262. wrong_button.type = HmiControlType::Button;
  263. wrong_button.binding = RegisterAddress{RegisterArea::D, 0};
  264. require(runtime.operateButton(wrong_button, HmiButtonEvent::Pressed).error
  265. == HmiRuntimeError::InvalidBinding,
  266. "a button bound to D must reject runtime writes");
  267. HmiControl wrong_numeric = unbound_indicator;
  268. wrong_numeric.type = HmiControlType::NumericInput;
  269. wrong_numeric.binding = RegisterAddress{RegisterArea::M, 0};
  270. require(runtime.writeNumericInput(wrong_numeric, 1).error
  271. == HmiRuntimeError::InvalidBinding,
  272. "a numeric input bound to M must reject runtime writes");
  273. }
  274. void testAppearanceEditing()
  275. {
  276. TestProjectStorage storage;
  277. ProjectService project_service(storage);
  278. HmiEditorService service(project_service);
  279. const std::string page_id = service.ensureDefaultPage().id;
  280. const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label);
  281. require(label.succeeded, "a label must be available for appearance editing");
  282. HmiControl appearance = *service.findControl(page_id, label.id);
  283. appearance.properties[HmiAppearanceProperty::kTextColor] = "#E53935";
  284. appearance.properties[HmiAppearanceProperty::kFontSize] = "18";
  285. appearance.properties[HmiAppearanceProperty::kFontBold] = "true";
  286. appearance.properties[HmiAppearanceProperty::kFontItalic] = "false";
  287. require(service.updateControl(page_id, label.id, appearance).succeeded,
  288. "valid appearance properties must be applied atomically");
  289. const HmiControl *updated = service.findControl(page_id, label.id);
  290. require(updated != nullptr
  291. && updated->properties.at(HmiAppearanceProperty::kTextColor) == "#E53935"
  292. && updated->properties.at(HmiAppearanceProperty::kFontSize) == "18"
  293. && updated->properties.at(HmiAppearanceProperty::kFontBold) == "true",
  294. "appearance properties must be stored on the HMI control");
  295. HmiControl invalid = *updated;
  296. invalid.properties[HmiAppearanceProperty::kTextColor] = "invalid";
  297. require(!service.updateControl(page_id, label.id, invalid).succeeded,
  298. "invalid appearance properties must be rejected without a partial update");
  299. require(service.findControl(page_id, label.id)->properties.at(
  300. HmiAppearanceProperty::kTextColor) == "#E53935",
  301. "failed appearance updates must leave the old color intact");
  302. require(service.undo().succeeded,
  303. "appearance updates must participate in HMI undo history");
  304. require(service.findControl(page_id, label.id)->properties.empty(),
  305. "undo must remove the applied appearance properties");
  306. require(service.redo().succeeded
  307. && service.findControl(page_id, label.id)->properties.at(
  308. HmiAppearanceProperty::kFontSize) == "18",
  309. "redo must restore the applied appearance properties");
  310. }
  311. void testPageLifecycleAndNavigation()
  312. {
  313. TestProjectStorage storage;
  314. ProjectService project_service(storage);
  315. HmiEditorService service(project_service);
  316. const std::string main_id = service.ensureDefaultPage().id;
  317. require(project_service.project().initialHmiPageId == main_id,
  318. "the first page must become the initial HMI page");
  319. const HmiEditorResult settings = service.addPage("Settings");
  320. const HmiEditorResult maintenance = service.addPage("Maintenance");
  321. require(settings.succeeded && maintenance.succeeded,
  322. "multiple HMI pages must be creatable");
  323. require(service.renamePage(settings.id, "Parameters").succeeded,
  324. "an HMI page must be renamable by stable id");
  325. require(service.renamePage(maintenance.id, "Parameters").error
  326. == HmiEditorError::DuplicateName,
  327. "HMI page names must remain unique");
  328. require(service.movePage(maintenance.id, -1).succeeded
  329. && project_service.project().hmiPages.at(1).id == maintenance.id,
  330. "HMI page order must be editable independently from page ids");
  331. const HmiEditorResult label = service.addControl(main_id, HmiControlType::Label);
  332. require(label.succeeded
  333. && service.findControl(main_id, label.id)->isConfigured(),
  334. "the editor service must expose a configured static Label control");
  335. const HmiEditorResult alarm_list = service.addControl(
  336. main_id, HmiControlType::AlarmList);
  337. require(alarm_list.succeeded
  338. && service.findControl(main_id, alarm_list.id)->isConfigured()
  339. && !service.findControl(main_id, alarm_list.id)->binding.has_value(),
  340. "AlarmList must be configured without a register binding");
  341. HmiControl bound_alarm_list = *service.findControl(main_id, alarm_list.id);
  342. bound_alarm_list.binding = RegisterAddress{RegisterArea::M, 20};
  343. require(!service.updateControl(
  344. main_id, alarm_list.id, bound_alarm_list).succeeded,
  345. "AlarmList must reject direct register bindings");
  346. const HmiEditorResult jump = service.addControl(main_id, HmiControlType::PageJump);
  347. require(jump.succeeded, "the editor service must expose PageJump creation");
  348. HmiControl jump_control = *service.findControl(main_id, jump.id);
  349. jump_control.pageJump = HmiPageJumpConfig{maintenance.id};
  350. require(service.updateControl(main_id, jump.id, jump_control).succeeded,
  351. "PageJump target updates must resolve stable page ids");
  352. require(service.removePage(maintenance.id).error == HmiEditorError::PageReferenced,
  353. "a page referenced by PageJump must not be deletable");
  354. require(service.setInitialPage(settings.id).succeeded,
  355. "the user must be able to select a different initial page");
  356. require(service.removePage(settings.id).error
  357. == HmiEditorError::InitialPageCannotBeRemoved,
  358. "the initial page must not be deleted implicitly");
  359. HmiNavigationService navigation(project_service);
  360. const HmiNavigationResult start = navigation.start();
  361. require(start.succeeded && start.pageId == settings.id,
  362. "runtime navigation must start from the persisted initial page");
  363. require(navigation.navigateTo(maintenance.id).succeeded
  364. && navigation.currentPageId() == maintenance.id,
  365. "runtime navigation must switch to an existing page");
  366. require(navigation.navigateTo("missing").error
  367. == HmiNavigationError::PageNotFound,
  368. "runtime navigation must reject an unknown page id");
  369. navigation.stop();
  370. require(navigation.currentPageId().empty(),
  371. "stopping runtime navigation must clear session state");
  372. }
  373. void testPageResizeIsAtomicAndUndoable()
  374. {
  375. TestProjectStorage storage;
  376. ProjectService project_service(storage);
  377. HmiEditorService service(project_service);
  378. const std::string page_id = service.ensureDefaultPage().id;
  379. const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label);
  380. require(label.succeeded, "page resize fixture must add a control");
  381. HmiControl control = *service.findControl(page_id, label.id);
  382. control.bounds = {700, 340, 80, 32};
  383. require(service.updateControl(page_id, label.id, control).succeeded,
  384. "page resize fixture must place the control near the page edge");
  385. require(service.resizePage(page_id, 1024, 600).succeeded,
  386. "an HMI page must be resizable to a larger valid rectangle");
  387. require(service.findPage(page_id)->width == 1024
  388. && service.findPage(page_id)->height == 600,
  389. "page resize must update both dimensions");
  390. require(service.resizePage(page_id, 700, 300).error
  391. == HmiEditorError::InvalidPage,
  392. "page resize must reject dimensions that clip an existing control");
  393. require(service.findPage(page_id)->width == 1024
  394. && service.findPage(page_id)->height == 600,
  395. "a rejected page resize must leave the original dimensions intact");
  396. require(service.undo().succeeded
  397. && service.findPage(page_id)->width == 800
  398. && service.findPage(page_id)->height == 400,
  399. "page resize must participate in HMI undo history");
  400. require(service.redo().succeeded
  401. && service.findPage(page_id)->width == 1024
  402. && service.findPage(page_id)->height == 600,
  403. "page resize redo must restore the new dimensions");
  404. require(service.resizePage(page_id, 319, 600).error
  405. == HmiEditorError::InvalidPage,
  406. "page width below the business minimum must be rejected");
  407. }
  408. void testConfiguredPageDefaultsAndLimits()
  409. {
  410. TestProjectStorage storage;
  411. ProjectLimitSettings limits;
  412. limits.maximumHmiPages = 1U;
  413. limits.maximumHmiControlsPerPage = 1U;
  414. HmiDefaultSettings defaults;
  415. defaults.pageWidth = 1024;
  416. defaults.pageHeight = 600;
  417. ProjectService project_service(storage, limits);
  418. HmiEditorService service(project_service, defaults);
  419. const HmiEditorResult page = service.ensureDefaultPage();
  420. require(page.succeeded
  421. && service.findPage(page.id)->width == 1024
  422. && service.findPage(page.id)->height == 600,
  423. "configured HMI dimensions must initialize the default page");
  424. require(!service.addPage("Second page").succeeded,
  425. "the HMI editor must use the configured page limit");
  426. require(service.addControl(page.id, HmiControlType::Label).succeeded
  427. && !service.addControl(page.id, HmiControlType::Label).succeeded,
  428. "the HMI editor must use the configured per-page control limit");
  429. }
  430. } // namespace
  431. int main()
  432. {
  433. try
  434. {
  435. // 编辑和运行场景分别验证服务层两条独立职责
  436. testControlEditing();
  437. testHistoryAndAtomicBatchDelete();
  438. testBatchPasteControls();
  439. testRuntimeValidationAndPasteBoundaries();
  440. testAppearanceEditing();
  441. testRuntimeUsesRegisterRepository();
  442. testPageLifecycleAndNavigation();
  443. testPageResizeIsAtomicAndUndoable();
  444. testConfiguredPageDefaultsAndLimits();
  445. }
  446. catch (const std::exception &error)
  447. {
  448. std::cerr << "HMI editor service tests failed: " << error.what() << '\n';
  449. return 1;
  450. }
  451. std::cout << "HMI editor service tests passed\n";
  452. return 0;
  453. }