| @@ -25,10 +25,35 @@ bool requiresWordBinding(HmiControlType type) | |||||
| return type == HmiControlType::NumericDisplay || type == HmiControlType::NumericInput; | return type == HmiControlType::NumericDisplay || type == HmiControlType::NumericInput; | ||||
| } | } | ||||
| bool supportsRegisterBinding(HmiControlType type) | |||||
| { | |||||
| return requiresBitBinding(type) || requiresWordBinding(type); | |||||
| } | |||||
| bool isSupportedControlType(HmiControlType type) | |||||
| { | |||||
| switch (type) | |||||
| { | |||||
| case HmiControlType::Button: | |||||
| case HmiControlType::Indicator: | |||||
| case HmiControlType::NumericDisplay: | |||||
| case HmiControlType::NumericInput: | |||||
| case HmiControlType::Label: | |||||
| case HmiControlType::PageJump: | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| } // namespace | } // namespace | ||||
| bool HmiControl::validate(std::string *error) const | bool HmiControl::validate(std::string *error) const | ||||
| { | { | ||||
| if (!isSupportedControlType(type)) | |||||
| { | |||||
| setError(error, "不支持的 HMI 控件类型"); | |||||
| return false; | |||||
| } | |||||
| if (id.empty()) | if (id.empty()) | ||||
| { | { | ||||
| setError(error, "HMI 控件 ID 不能为空"); | setError(error, "HMI 控件 ID 不能为空"); | ||||
| @@ -68,6 +93,24 @@ bool HmiControl::validate(std::string *error) const | |||||
| setError(error, "HMI 控件绑定了无效地址"); | setError(error, "HMI 控件绑定了无效地址"); | ||||
| return false; | return false; | ||||
| } | } | ||||
| if (!supportsRegisterBinding(type) && binding.has_value()) | |||||
| { | |||||
| setError(error, "文本和页面跳转控件不能绑定寄存器"); | |||||
| return false; | |||||
| } | |||||
| if (type == HmiControlType::PageJump) | |||||
| { | |||||
| if (!pageJump.has_value()) | |||||
| { | |||||
| setError(error, "页面跳转控件缺少跳转配置"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| else if (pageJump.has_value()) | |||||
| { | |||||
| setError(error, "非页面跳转控件不能包含跳转配置"); | |||||
| return false; | |||||
| } | |||||
| return true; | return true; | ||||
| } | } | ||||
| @@ -77,6 +120,10 @@ bool HmiControl::isConfigured() const | |||||
| { | { | ||||
| return true; | return true; | ||||
| } | } | ||||
| if (type == HmiControlType::PageJump) | |||||
| { | |||||
| return pageJump.has_value() && !pageJump->targetPageId.empty(); | |||||
| } | |||||
| if (!binding.has_value() || !binding->isValid()) | if (!binding.has_value() || !binding->isValid()) | ||||
| { | { | ||||
| return false; | return false; | ||||
| @@ -41,7 +41,8 @@ enum class HmiControlType | |||||
| Indicator, // 指示灯 | Indicator, // 指示灯 | ||||
| NumericDisplay, // 数值显示 | NumericDisplay, // 数值显示 | ||||
| NumericInput, // 数值输入 | NumericInput, // 数值输入 | ||||
| Label // 标签 | |||||
| Label, // 标签 | |||||
| PageJump // 页面跳转 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -55,6 +56,11 @@ enum class HmiButtonOperation | |||||
| MomentaryOn // 按下时写入 1,释放时写入 0 | MomentaryOn // 按下时写入 1,释放时写入 0 | ||||
| }; | }; | ||||
| struct HmiPageJumpConfig | |||||
| { | |||||
| std::string targetPageId; | |||||
| }; | |||||
| /** | /** | ||||
| * @brief 描述一个可保存的 HMI 控件及其显示和寄存器配置 | * @brief 描述一个可保存的 HMI 控件及其显示和寄存器配置 | ||||
| * | * | ||||
| @@ -69,6 +75,7 @@ struct HmiControl | |||||
| std::optional<RegisterAddress> binding; | std::optional<RegisterAddress> binding; | ||||
| std::map<std::string, std::string> properties; | std::map<std::string, std::string> properties; | ||||
| HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn; | HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn; | ||||
| std::optional<HmiPageJumpConfig> pageJump; | |||||
| /** | /** | ||||
| * @brief 校验控件的标识、尺寸、扩展属性和寄存器绑定 | * @brief 校验控件的标识、尺寸、扩展属性和寄存器绑定 | ||||
| @@ -33,6 +33,26 @@ bool containsDuplicateId(const std::vector<TItem> &items) | |||||
| return false; | return false; | ||||
| } | } | ||||
| template <typename TItem> | |||||
| bool containsDuplicateName(const std::vector<TItem> &items) | |||||
| { | |||||
| for (auto current = items.cbegin(); current != items.cend(); ++current) | |||||
| { | |||||
| const auto duplicate = std::find_if( | |||||
| current + 1, | |||||
| items.cend(), | |||||
| [¤t](const TItem &item) | |||||
| { | |||||
| return item.name == current->name; | |||||
| }); | |||||
| if (duplicate != items.cend()) | |||||
| { | |||||
| return true; | |||||
| } | |||||
| } | |||||
| return false; | |||||
| } | |||||
| } // namespace | } // namespace | ||||
| bool Project::validate(std::string *error) const | bool Project::validate(std::string *error) const | ||||
| @@ -47,11 +67,40 @@ bool Project::validate(std::string *error) const | |||||
| setError(error, "工程内的 HMI 页面 ID 必须唯一"); | setError(error, "工程内的 HMI 页面 ID 必须唯一"); | ||||
| return false; | return false; | ||||
| } | } | ||||
| if (containsDuplicateName(hmiPages)) | |||||
| { | |||||
| setError(error, "工程内的 HMI 页面名称必须唯一"); | |||||
| return false; | |||||
| } | |||||
| if (hmiPages.empty()) | |||||
| { | |||||
| if (!initialHmiPageId.empty()) | |||||
| { | |||||
| setError(error, "空工程不能设置初始 HMI 页面"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| else | |||||
| { | |||||
| const auto initial_page = std::find_if( | |||||
| hmiPages.cbegin(), hmiPages.cend(), | |||||
| [this](const HmiPage &page) { return page.id == initialHmiPageId; }); | |||||
| if (initial_page == hmiPages.cend()) | |||||
| { | |||||
| setError(error, "工程初始 HMI 页面不存在"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (containsDuplicateId(controlLogics)) | if (containsDuplicateId(controlLogics)) | ||||
| { | { | ||||
| setError(error, "工程内的控制逻辑 ID 必须唯一"); | setError(error, "工程内的控制逻辑 ID 必须唯一"); | ||||
| return false; | return false; | ||||
| } | } | ||||
| if (containsDuplicateName(controlLogics)) | |||||
| { | |||||
| setError(error, "工程内的控制逻辑名称必须唯一"); | |||||
| return false; | |||||
| } | |||||
| for (const HmiPage &page : hmiPages) | for (const HmiPage &page : hmiPages) | ||||
| { | { | ||||
| // 工程聚合校验会向下委托页面和控件的完整规则 | // 工程聚合校验会向下委托页面和控件的完整规则 | ||||
| @@ -59,6 +108,26 @@ bool Project::validate(std::string *error) const | |||||
| { | { | ||||
| return false; | return false; | ||||
| } | } | ||||
| for (const HmiControl &control : page.controls) | |||||
| { | |||||
| if (control.type != HmiControlType::PageJump | |||||
| || !control.pageJump.has_value() | |||||
| || control.pageJump->targetPageId.empty()) | |||||
| { | |||||
| continue; | |||||
| } | |||||
| const auto target = std::find_if( | |||||
| hmiPages.cbegin(), hmiPages.cend(), | |||||
| [&control](const HmiPage &candidate) | |||||
| { | |||||
| return candidate.id == control.pageJump->targetPageId; | |||||
| }); | |||||
| if (target == hmiPages.cend()) | |||||
| { | |||||
| setError(error, "页面跳转控件 " + control.id + " 的目标页面不存在"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| for (const ControlLogic &logic : controlLogics) | for (const ControlLogic &logic : controlLogics) | ||||
| { | { | ||||
| @@ -85,7 +154,7 @@ bool Project::validateForRunning(std::string *error) const | |||||
| } | } | ||||
| for (const ControlLogic &logic : controlLogics) | for (const ControlLogic &logic : controlLogics) | ||||
| { | { | ||||
| if (!logic.validateForRunning(error)) | |||||
| if (logic.enabled && !logic.validateForRunning(error)) | |||||
| { | { | ||||
| return false; | return false; | ||||
| } | } | ||||
| @@ -24,6 +24,8 @@ struct Project | |||||
| ProjectMetadata metadata; | ProjectMetadata metadata; | ||||
| // 工程包含的 HMI 页面集合 | // 工程包含的 HMI 页面集合 | ||||
| std::vector<HmiPage> hmiPages; | std::vector<HmiPage> hmiPages; | ||||
| // 进入运行态时显示的初始 HMI 页面 | |||||
| std::string initialHmiPageId; | |||||
| // 工程包含的控制逻辑集合 | // 工程包含的控制逻辑集合 | ||||
| std::vector<ControlLogic> controlLogics; | std::vector<ControlLogic> controlLogics; | ||||
| @@ -278,6 +278,10 @@ QString hmiControlTypeName(HmiControlType type) | |||||
| { | { | ||||
| return QStringLiteral("label"); | return QStringLiteral("label"); | ||||
| } | } | ||||
| case HmiControlType::PageJump: | |||||
| { | |||||
| return QStringLiteral("pageJump"); | |||||
| } | |||||
| default: | default: | ||||
| { | { | ||||
| return {}; | return {}; | ||||
| @@ -309,6 +313,10 @@ bool parseHmiControlType( | |||||
| { | { | ||||
| *type = HmiControlType::Label; | *type = HmiControlType::Label; | ||||
| } | } | ||||
| else if (value == "pageJump") | |||||
| { | |||||
| *type = HmiControlType::PageJump; | |||||
| } | |||||
| else | else | ||||
| { | { | ||||
| return state->fail( | return state->fail( | ||||
| @@ -476,6 +484,14 @@ QJsonObject serializeHmiControl(const HmiControl &control) | |||||
| QStringLiteral("buttonOperation"), | QStringLiteral("buttonOperation"), | ||||
| hmiButtonOperationName(control.buttonOperation)); | hmiButtonOperationName(control.buttonOperation)); | ||||
| } | } | ||||
| if (control.type == HmiControlType::PageJump) | |||||
| { | |||||
| object.insert( | |||||
| QStringLiteral("targetPageId"), | |||||
| fromUtf8(control.pageJump.has_value() | |||||
| ? control.pageJump->targetPageId | |||||
| : std::string{})); | |||||
| } | |||||
| return object; | return object; | ||||
| } | } | ||||
| @@ -520,6 +536,15 @@ bool parseHmiControl( | |||||
| return false; | return false; | ||||
| } | } | ||||
| } | } | ||||
| if (control->type == HmiControlType::PageJump) | |||||
| { | |||||
| std::string target_page_id; | |||||
| if (!readString(object, "targetPageId", context, &target_page_id, state)) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| control->pageJump = HmiPageJumpConfig{std::move(target_page_id)}; | |||||
| } | |||||
| // binding 允许为 null,其余非空值必须是合法的寄存器地址对象 | // binding 允许为 null,其余非空值必须是合法的寄存器地址对象 | ||||
| if (binding.isNull()) | if (binding.isNull()) | ||||
| @@ -1144,6 +1169,8 @@ QJsonObject serializeProject(const Project &project) | |||||
| object.insert(QStringLiteral("id"), fromUtf8(project.metadata.id)); | object.insert(QStringLiteral("id"), fromUtf8(project.metadata.id)); | ||||
| object.insert(QStringLiteral("name"), fromUtf8(project.metadata.name)); | object.insert(QStringLiteral("name"), fromUtf8(project.metadata.name)); | ||||
| object.insert(QStringLiteral("hmiPages"), pages); | object.insert(QStringLiteral("hmiPages"), pages); | ||||
| object.insert(QStringLiteral("initialHmiPageId"), | |||||
| fromUtf8(project.initialHmiPageId)); | |||||
| object.insert(QStringLiteral("controlLogics"), logics); | object.insert(QStringLiteral("controlLogics"), logics); | ||||
| return object; | return object; | ||||
| } | } | ||||
| @@ -1174,6 +1201,8 @@ bool parseProject( | |||||
| if (!readString(object, "id", "project", &project->metadata.id, state) | if (!readString(object, "id", "project", &project->metadata.id, state) | ||||
| || !readString(object, "name", "project", &project->metadata.name, state) | || !readString(object, "name", "project", &project->metadata.name, state) | ||||
| || !readArray(object, "hmiPages", "project", &pages, state) | || !readArray(object, "hmiPages", "project", &pages, state) | ||||
| || !readString(object, "initialHmiPageId", "project", | |||||
| &project->initialHmiPageId, state) | |||||
| || !readArray(object, "controlLogics", "project", &logics, state)) | || !readArray(object, "controlLogics", "project", &logics, state)) | ||||
| { | { | ||||
| return false; | return false; | ||||
| @@ -115,10 +115,75 @@ Project makeValidProject() | |||||
| Project project; | Project project; | ||||
| project.metadata = {"sample-project", "Sample project", "1.0"}; | project.metadata = {"sample-project", "Sample project", "1.0"}; | ||||
| project.hmiPages.push_back(page); | project.hmiPages.push_back(page); | ||||
| project.initialHmiPageId = page.id; | |||||
| project.controlLogics.push_back(logic); | project.controlLogics.push_back(logic); | ||||
| return project; | return project; | ||||
| } | } | ||||
| void testMultiPageAndLogicDomainRules() | |||||
| { | |||||
| Project project = makeValidProject(); | |||||
| HmiPage settings; | |||||
| settings.id = "settings-page"; | |||||
| settings.name = "Settings"; | |||||
| project.hmiPages.push_back(settings); | |||||
| HmiControl label; | |||||
| label.id = "title"; | |||||
| label.type = HmiControlType::Label; | |||||
| label.text = "Machine"; | |||||
| project.hmiPages.front().controls.push_back(label); | |||||
| require(project.validate(), "an unbound label must be a valid static control"); | |||||
| project.hmiPages.front().controls.back().binding = | |||||
| RegisterAddress{RegisterArea::M, 10}; | |||||
| require(!project.validate(), "labels must reject register bindings"); | |||||
| project = makeValidProject(); | |||||
| project.hmiPages.push_back(settings); | |||||
| HmiControl jump; | |||||
| jump.id = "settings-jump"; | |||||
| jump.type = HmiControlType::PageJump; | |||||
| jump.text = "Settings"; | |||||
| jump.pageJump = HmiPageJumpConfig{settings.id}; | |||||
| project.hmiPages.front().controls.push_back(jump); | |||||
| require(project.validate() && project.validateForRunning(), | |||||
| "a page jump must resolve its target by stable page id"); | |||||
| project.hmiPages.front().controls.back().pageJump->targetPageId = "missing"; | |||||
| require(!project.validate(), "a page jump must reject a missing target page"); | |||||
| project = makeValidProject(); | |||||
| project.initialHmiPageId = "missing"; | |||||
| require(!project.validate(), "the initial HMI page id must resolve to a page"); | |||||
| project = makeValidProject(); | |||||
| HmiPage duplicate_name = settings; | |||||
| duplicate_name.name = project.hmiPages.front().name; | |||||
| project.hmiPages.push_back(duplicate_name); | |||||
| require(!project.validate(), "HMI page names must be unique"); | |||||
| project = makeValidProject(); | |||||
| ControlLogic disabled_draft; | |||||
| disabled_draft.id = "draft-logic"; | |||||
| disabled_draft.name = "Draft logic"; | |||||
| disabled_draft.enabled = false; | |||||
| disabled_draft.rungs.push_back( | |||||
| {"rung-1", "Draft network", std::nullopt, std::nullopt}); | |||||
| project.controlLogics.push_back(disabled_draft); | |||||
| require(project.validateForRunning(), | |||||
| "a disabled draft logic must not block offline running"); | |||||
| project.controlLogics.back().name = project.controlLogics.front().name; | |||||
| require(!project.validate(), "control logic names must be unique"); | |||||
| project = makeValidProject(); | |||||
| project.hmiPages.front().controls.front().type = | |||||
| static_cast<HmiControlType>(99); | |||||
| project.hmiPages.front().controls.front().binding.reset(); | |||||
| require(!project.validate(), "unknown HMI control types must be rejected"); | |||||
| } | |||||
| void testLogicNodeConfigurationBoundaries() | void testLogicNodeConfigurationBoundaries() | ||||
| { | { | ||||
| // 触点只能绑定 M 区,数值比较只能绑定 D 区 | // 触点只能绑定 M 区,数值比较只能绑定 D 区 | ||||
| @@ -302,6 +367,7 @@ int main() | |||||
| testLogicNodeConfigurationBoundaries(); | testLogicNodeConfigurationBoundaries(); | ||||
| testLadderLogicBoundaries(); | testLadderLogicBoundaries(); | ||||
| testModelsValidateBindingsAndIdentifiers(); | testModelsValidateBindingsAndIdentifiers(); | ||||
| testMultiPageAndLogicDomainRules(); | |||||
| testRuntimeStateBoundaries(); | testRuntimeStateBoundaries(); | ||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||
| @@ -3,6 +3,9 @@ | |||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include <QFile> | #include <QFile> | ||||
| #include <QJsonArray> | |||||
| #include <QJsonDocument> | |||||
| #include <QJsonObject> | |||||
| #include <QTemporaryDir> | #include <QTemporaryDir> | ||||
| #include <exception> | #include <exception> | ||||
| @@ -56,6 +59,19 @@ Project makeExampleProject() | |||||
| target_input.binding = RegisterAddress{RegisterArea::D, 3}; | target_input.binding = RegisterAddress{RegisterArea::D, 3}; | ||||
| target_input.properties.emplace("minimum", "-100"); | target_input.properties.emplace("minimum", "-100"); | ||||
| HmiControl title_label; | |||||
| title_label.id = "title-label"; | |||||
| title_label.type = HmiControlType::Label; | |||||
| title_label.bounds = {10, 145, 180, 32}; | |||||
| title_label.text = "Production line"; | |||||
| HmiControl settings_jump; | |||||
| settings_jump.id = "settings-jump"; | |||||
| settings_jump.type = HmiControlType::PageJump; | |||||
| settings_jump.bounds = {210, 145, 120, 40}; | |||||
| settings_jump.text = "Settings"; | |||||
| settings_jump.pageJump = HmiPageJumpConfig{"settings-page"}; | |||||
| HmiPage page; | HmiPage page; | ||||
| page.id = "main-page"; | page.id = "main-page"; | ||||
| page.name = "Main"; | page.name = "Main"; | ||||
| @@ -63,6 +79,12 @@ Project makeExampleProject() | |||||
| page.controls.push_back(running_indicator); | page.controls.push_back(running_indicator); | ||||
| page.controls.push_back(temperature_display); | page.controls.push_back(temperature_display); | ||||
| page.controls.push_back(target_input); | page.controls.push_back(target_input); | ||||
| page.controls.push_back(title_label); | |||||
| page.controls.push_back(settings_jump); | |||||
| HmiPage settings_page; | |||||
| settings_page.id = "settings-page"; | |||||
| settings_page.name = "Settings"; | |||||
| LogicNode contact; | LogicNode contact; | ||||
| contact.id = "start-contact"; | contact.id = "start-contact"; | ||||
| @@ -115,7 +137,16 @@ Project makeExampleProject() | |||||
| Project project; | Project project; | ||||
| project.metadata = {"example-project", "Example project", "1.0"}; | project.metadata = {"example-project", "Example project", "1.0"}; | ||||
| project.hmiPages.push_back(page); | project.hmiPages.push_back(page); | ||||
| project.hmiPages.push_back(settings_page); | |||||
| project.initialHmiPageId = page.id; | |||||
| project.controlLogics.push_back(logic); | project.controlLogics.push_back(logic); | ||||
| ControlLogic draft_logic; | |||||
| draft_logic.id = "draft-logic"; | |||||
| draft_logic.name = "Draft logic"; | |||||
| draft_logic.enabled = false; | |||||
| draft_logic.rungs.push_back( | |||||
| {"rung-1", "Draft network", std::nullopt, std::nullopt}); | |||||
| project.controlLogics.push_back(draft_logic); | |||||
| return project; | return project; | ||||
| } | } | ||||
| @@ -172,6 +203,8 @@ void testExampleProjectRoundTrip() | |||||
| const QString first_path = directory.filePath("example.json"); | const QString first_path = directory.filePath("example.json"); | ||||
| const QString second_path = directory.filePath("example-copy.json"); | const QString second_path = directory.filePath("example-copy.json"); | ||||
| const QString invalid_operation_path = directory.filePath("invalid-operation.json"); | const QString invalid_operation_path = directory.filePath("invalid-operation.json"); | ||||
| const QString missing_initial_path = directory.filePath("missing-initial.json"); | |||||
| const QString missing_target_path = directory.filePath("missing-target.json"); | |||||
| require(service.saveAs(first_path.toStdString()).succeeded, | require(service.saveAs(first_path.toStdString()).succeeded, | ||||
| "example project save must succeed"); | "example project save must succeed"); | ||||
| const QByteArray saved_json = readBytes(first_path); | const QByteArray saved_json = readBytes(first_path); | ||||
| @@ -183,8 +216,10 @@ void testExampleProjectRoundTrip() | |||||
| require(!saved_json.contains("\"dataPoints\""), | require(!saved_json.contains("\"dataPoints\""), | ||||
| "saved project must not contain the removed data point model"); | "saved project must not contain the removed data point model"); | ||||
| require(saved_json.contains("\"formatVersion\": \"1.0\"") | require(saved_json.contains("\"formatVersion\": \"1.0\"") | ||||
| && saved_json.contains("\"buttonOperation\": \"setOn\""), | |||||
| "version 1.0 projects must persist the button operation"); | |||||
| && saved_json.contains("\"buttonOperation\": \"setOn\"") | |||||
| && saved_json.contains("\"initialHmiPageId\": \"main-page\"") | |||||
| && saved_json.contains("\"targetPageId\": \"settings-page\""), | |||||
| "version 1.0 projects must persist the current multi-page schema"); | |||||
| require(!saved_json.contains("\"stages\"") | require(!saved_json.contains("\"stages\"") | ||||
| && !saved_json.contains("\"branches\""), | && !saved_json.contains("\"branches\""), | ||||
| "current project format must not contain the removed stage model"); | "current project format must not contain the removed stage model"); | ||||
| @@ -196,9 +231,11 @@ void testExampleProjectRoundTrip() | |||||
| const Project &project = service.project(); | const Project &project = service.project(); | ||||
| require(project.metadata.id == "example-project", "project id must survive round trip"); | require(project.metadata.id == "example-project", "project id must survive round trip"); | ||||
| require(project.hmiPages.size() == 1, "HMI page count must survive round trip"); | |||||
| require(project.hmiPages.front().controls.size() == 4, | |||||
| "all basic HMI controls must survive round trip"); | |||||
| require(project.hmiPages.size() == 2, "HMI page count must survive round trip"); | |||||
| require(project.initialHmiPageId == "main-page", | |||||
| "the initial HMI page id must survive round trip"); | |||||
| require(project.hmiPages.front().controls.size() == 6, | |||||
| "register, Label and PageJump controls must survive round trip"); | |||||
| require(project.hmiPages.front().controls.front().binding->area() | require(project.hmiPages.front().controls.front().binding->area() | ||||
| == RegisterArea::M, | == RegisterArea::M, | ||||
| "HMI M binding must survive round trip"); | "HMI M binding must survive round trip"); | ||||
| @@ -213,7 +250,11 @@ void testExampleProjectRoundTrip() | |||||
| "numeric display D binding must survive round trip"); | "numeric display D binding must survive round trip"); | ||||
| require(project.hmiPages.front().controls.at(3).bounds.x == 150, | require(project.hmiPages.front().controls.at(3).bounds.x == 150, | ||||
| "numeric input bounds must survive round trip"); | "numeric input bounds must survive round trip"); | ||||
| require(project.controlLogics.size() == 1, | |||||
| require(project.hmiPages.front().controls.at(4).type == HmiControlType::Label | |||||
| && project.hmiPages.front().controls.at(5).pageJump->targetPageId | |||||
| == "settings-page", | |||||
| "Label and PageJump typed data must survive round trip"); | |||||
| require(project.controlLogics.size() == 2, | |||||
| "control logic count must survive round trip"); | "control logic count must survive round trip"); | ||||
| require(!project.controlLogics.front().enabled, | require(!project.controlLogics.front().enabled, | ||||
| "control logic enabled state must survive round trip"); | "control logic enabled state must survive round trip"); | ||||
| @@ -244,6 +285,35 @@ void testExampleProjectRoundTrip() | |||||
| writeText(invalid_operation_path, invalid_operation); | writeText(invalid_operation_path, invalid_operation); | ||||
| require(!service.load(invalid_operation_path.toStdString()).succeeded, | require(!service.load(invalid_operation_path.toStdString()).succeeded, | ||||
| "unsupported HMI button operations must be rejected"); | "unsupported HMI button operations must be rejected"); | ||||
| QJsonObject missing_initial = QJsonDocument::fromJson(saved_json).object(); | |||||
| missing_initial.remove(QStringLiteral("initialHmiPageId")); | |||||
| writeText( | |||||
| missing_initial_path, | |||||
| QJsonDocument(missing_initial).toJson(QJsonDocument::Compact)); | |||||
| ProjectOperationResult missing_result = service.load( | |||||
| missing_initial_path.toStdString()); | |||||
| require(!missing_result.succeeded | |||||
| && missing_result.storageError == ProjectStorageError::MissingField, | |||||
| "the 1.0 schema must require initialHmiPageId without migration defaults"); | |||||
| QJsonObject missing_target = QJsonDocument::fromJson(saved_json).object(); | |||||
| QJsonArray pages = missing_target.value(QStringLiteral("hmiPages")).toArray(); | |||||
| QJsonObject main_page = pages.at(0).toObject(); | |||||
| QJsonArray controls = main_page.value(QStringLiteral("controls")).toArray(); | |||||
| QJsonObject jump = controls.at(5).toObject(); | |||||
| jump.remove(QStringLiteral("targetPageId")); | |||||
| controls.replace(5, jump); | |||||
| main_page.insert(QStringLiteral("controls"), controls); | |||||
| pages.replace(0, main_page); | |||||
| missing_target.insert(QStringLiteral("hmiPages"), pages); | |||||
| writeText( | |||||
| missing_target_path, | |||||
| QJsonDocument(missing_target).toJson(QJsonDocument::Compact)); | |||||
| missing_result = service.load(missing_target_path.toStdString()); | |||||
| require(!missing_result.succeeded | |||||
| && missing_result.storageError == ProjectStorageError::MissingField, | |||||
| "the 1.0 schema must require PageJump targetPageId"); | |||||
| } | } | ||||
| void testInvalidFiles() | void testInvalidFiles() | ||||