综合平台编程器项目的远程存储
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

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