#include "domain/project_storage.h" #include "domain/register_repository.h" #include "services/hmi_editor_service.h" #include "services/hmi_runtime_service.h" #include "services/hmi_navigation_service.h" #include "services/project_service.h" #include #include #include #include namespace { class TestProjectStorage final : public ProjectStorage { public: // 编辑服务测试只关注内存模型变更,不依赖真实文件系统 ProjectSaveResult save(const Project &, const std::string &) override { return {true, ProjectStorageError::None, {}}; } ProjectLoadResult load(const std::string &) override { return {false, {}, ProjectStorageError::FileReadFailed, {}}; } }; void require(bool condition, const std::string &message) { if (!condition) { throw std::runtime_error(message); } } void testControlEditing() { // 覆盖控件创建、移动、绑定校验、重命名冲突和删除流程 TestProjectStorage storage; ProjectService project_service(storage); HmiEditorService service(project_service); const HmiEditorResult page_result = service.ensureDefaultPage(); require(page_result.succeeded, "default HMI page creation must succeed"); const std::string page_id = page_result.id; const HmiEditorResult button = service.addControl(page_id, HmiControlType::Button); const HmiEditorResult indicator = service.addControl(page_id, HmiControlType::Indicator); const HmiEditorResult display = service.addControl( page_id, HmiControlType::NumericDisplay); const HmiEditorResult input = service.addControl(page_id, HmiControlType::NumericInput); require(button.succeeded && indicator.succeeded && display.succeeded && input.succeeded, "four basic HMI controls must be added"); const HmiPage *page = service.findPage(page_id); require(page != nullptr && page->controls.size() == 4, "all added controls must be kept in the page model"); require(service.moveControl(page_id, button.id, {120, 80, 120, 40}).succeeded, "a valid control move must succeed"); require(!service.moveControl(page_id, button.id, {790, 460, 120, 40}).succeeded, "a move outside the page must be rejected"); HmiControl updated = *service.findControl(page_id, button.id); updated.binding = RegisterAddress{RegisterArea::D, 0}; require(!service.updateControl(page_id, button.id, updated).succeeded, "buttons must reject D address bindings during editing"); updated.binding = RegisterAddress{RegisterArea::M, 7}; updated.text = "启动"; require(service.updateControl(page_id, button.id, updated).succeeded, "a button M binding and edited text must be accepted"); HmiControl duplicate = *service.findControl(page_id, indicator.id); duplicate.id = button.id; require(service.updateControl(page_id, indicator.id, duplicate).error == HmiEditorError::DuplicateId, "duplicate control ids must be rejected"); require(service.removeControl(page_id, indicator.id).succeeded, "deleting a selected control must succeed"); require(service.findControl(page_id, indicator.id) == nullptr, "deleted controls must not remain in the model"); } void testRuntimeUsesRegisterRepository() { // 运行服务只能经由仓库接口读写 M/D,不依赖具体离线实现 VirtualRegisterRepository repository; HmiRuntimeService runtime_service(repository); HmiControl button; button.id = "start"; button.type = HmiControlType::Button; button.binding = RegisterAddress{RegisterArea::M, 12}; require(button.buttonOperation == HmiButtonOperation::MomentaryOn, "new buttons must default to momentary ON"); require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, "momentary button press must be accepted by the repository"); require(repository.readBit(*button.binding).value, "momentary button press must write ON"); require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded, "momentary button release must be accepted by the repository"); require(!repository.readBit(*button.binding).value, "momentary button release must write OFF"); button.buttonOperation = HmiButtonOperation::SetOn; require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, "set ON button press must succeed"); require(repository.readBit(*button.binding).value, "set ON button press must write ON"); require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded, "set ON button release must be ignored successfully"); require(repository.readBit(*button.binding).value, "set ON button release must keep the bit ON"); button.buttonOperation = HmiButtonOperation::SetOff; require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, "set OFF button press must succeed"); require(!repository.readBit(*button.binding).value, "set OFF button press must write OFF"); button.buttonOperation = HmiButtonOperation::Toggle; require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, "toggle button press must succeed"); require(repository.readBit(*button.binding).value, "toggle button press must invert the current value"); HmiControl indicator; indicator.id = "running"; indicator.type = HmiControlType::Indicator; indicator.binding = RegisterAddress{RegisterArea::M, 12}; const HmiRuntimeReadResult indicator_value = runtime_service.readControl(indicator); require(indicator_value.succeeded && indicator_value.bit_value, "indicators must read M values through the repository"); HmiControl numeric_input; numeric_input.id = "target"; numeric_input.type = HmiControlType::NumericInput; numeric_input.binding = RegisterAddress{RegisterArea::D, 9}; require(runtime_service.writeNumericInput(numeric_input, -18).succeeded, "numeric input runtime writes must be accepted by the repository"); HmiControl numeric_display; numeric_display.id = "actual"; numeric_display.type = HmiControlType::NumericDisplay; numeric_display.binding = RegisterAddress{RegisterArea::D, 9}; const HmiRuntimeReadResult numeric_value = runtime_service.readControl(numeric_display); require(numeric_value.succeeded && numeric_value.word_value == -18, "numeric display must read D values through the repository"); } void testPageLifecycleAndNavigation() { TestProjectStorage storage; ProjectService project_service(storage); HmiEditorService service(project_service); const std::string main_id = service.ensureDefaultPage().id; require(project_service.project().initialHmiPageId == main_id, "the first page must become the initial HMI page"); const HmiEditorResult settings = service.addPage("Settings"); const HmiEditorResult maintenance = service.addPage("Maintenance"); require(settings.succeeded && maintenance.succeeded, "multiple HMI pages must be creatable"); require(service.renamePage(settings.id, "Parameters").succeeded, "an HMI page must be renamable by stable id"); require(service.renamePage(maintenance.id, "Parameters").error == HmiEditorError::DuplicateName, "HMI page names must remain unique"); require(service.movePage(maintenance.id, -1).succeeded && project_service.project().hmiPages.at(1).id == maintenance.id, "HMI page order must be editable independently from page ids"); const HmiEditorResult label = service.addControl(main_id, HmiControlType::Label); require(label.succeeded && service.findControl(main_id, label.id)->isConfigured(), "the editor service must expose a configured static Label control"); const HmiEditorResult jump = service.addControl(main_id, HmiControlType::PageJump); require(jump.succeeded, "the editor service must expose PageJump creation"); HmiControl jump_control = *service.findControl(main_id, jump.id); jump_control.pageJump = HmiPageJumpConfig{maintenance.id}; require(service.updateControl(main_id, jump.id, jump_control).succeeded, "PageJump target updates must resolve stable page ids"); require(service.removePage(maintenance.id).error == HmiEditorError::PageReferenced, "a page referenced by PageJump must not be deletable"); require(service.setInitialPage(settings.id).succeeded, "the user must be able to select a different initial page"); require(service.removePage(settings.id).error == HmiEditorError::InitialPageCannotBeRemoved, "the initial page must not be deleted implicitly"); HmiNavigationService navigation(project_service); const HmiNavigationResult start = navigation.start(); require(start.succeeded && start.pageId == settings.id, "runtime navigation must start from the persisted initial page"); require(navigation.navigateTo(maintenance.id).succeeded && navigation.currentPageId() == maintenance.id, "runtime navigation must switch to an existing page"); require(navigation.navigateTo("missing").error == HmiNavigationError::PageNotFound, "runtime navigation must reject an unknown page id"); navigation.stop(); require(navigation.currentPageId().empty(), "stopping runtime navigation must clear session state"); } } // namespace int main() { try { // 编辑和运行场景分别验证服务层两条独立职责 testControlEditing(); testRuntimeUsesRegisterRepository(); testPageLifecycleAndNavigation(); } catch (const std::exception &error) { std::cerr << "HMI editor service tests failed: " << error.what() << '\n'; return 1; } std::cout << "HMI editor service tests passed\n"; return 0; }