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

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