综合平台编程器项目的远程存储
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

392 lignes
19 KiB

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