| @@ -33,7 +33,7 @@ bool AlarmDefinition::validate(std::string *error) const | |||
| } | |||
| if (message.size() > ProjectLimits::kMaximumTextBytes) | |||
| { | |||
| setError(error, "报警文本不能超过 4096 个 UTF-8 字节"); | |||
| setError(error, "报警文本不能超过 256 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| if (!address.isValid()) | |||
| @@ -221,13 +221,13 @@ bool validateConditionExpression( | |||
| { | |||
| if (depth > ProjectLimits::kMaximumExpressionDepth) | |||
| { | |||
| setError(error, "条件表达式最多嵌套 20 层"); | |||
| setError(error, "条件表达式最多嵌套 12 层"); | |||
| return false; | |||
| } | |||
| ++*node_count; | |||
| if (*node_count > ProjectLimits::kMaximumExpressionNodesPerRung) | |||
| { | |||
| setError(error, "单个网络最多包含 4096 个条件表达式节点和叶子"); | |||
| setError(error, "单个网络最多包含 1024 个条件表达式节点和叶子"); | |||
| return false; | |||
| } | |||
| if (expression.id.empty()) | |||
| @@ -282,7 +282,7 @@ bool validateConditionExpression( | |||
| } | |||
| if (expression.children.size() > ProjectLimits::kMaximumExpressionChildren) | |||
| { | |||
| setError(error, "单个串联或并联容器最多包含 256 个子表达式"); | |||
| setError(error, "单个串联或并联容器最多包含 64 个子表达式"); | |||
| return false; | |||
| } | |||
| @@ -319,7 +319,7 @@ bool validateConditionExpression( | |||
| total_columns = std::max(total_columns, child_columns); | |||
| if (total_rows > ProjectLimits::kMaximumLogicRows - child_rows) | |||
| { | |||
| setError(error, "单个网络的逻辑总行数最多为 256 行"); | |||
| setError(error, "单个网络的逻辑总行数最多为 64 行"); | |||
| return false; | |||
| } | |||
| total_rows += child_rows; | |||
| @@ -328,7 +328,7 @@ bool validateConditionExpression( | |||
| if (total_columns > ProjectLimits::kMaximumConditionColumns | |||
| || total_rows > ProjectLimits::kMaximumLogicRows) | |||
| { | |||
| setError(error, "单个网络最多使用 10 列条件、1 列输出和 256 行逻辑网格"); | |||
| setError(error, "单个网络最多使用 10 列条件、1 列输出和 64 行逻辑网格"); | |||
| return false; | |||
| } | |||
| *columns = total_columns; | |||
| @@ -680,7 +680,7 @@ bool LadderRung::validateStructure(std::string *error) const | |||
| } | |||
| if (name.size() > ProjectLimits::kMaximumTextBytes) | |||
| { | |||
| setError(error, "梯形图网络名称不能超过 4096 个 UTF-8 字节"); | |||
| setError(error, "梯形图网络名称不能超过 256 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| if (containsLineBreak(comment)) | |||
| @@ -755,12 +755,12 @@ bool ControlLogic::validateStructure(std::string *error) const | |||
| } | |||
| if (name.size() > ProjectLimits::kMaximumTextBytes) | |||
| { | |||
| setError(error, "控制逻辑名称不能超过 4096 个 UTF-8 字节"); | |||
| setError(error, "控制逻辑名称不能超过 256 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| if (rungs.size() > ProjectLimits::kMaximumRungsPerLogic) | |||
| { | |||
| setError(error, "单组控制逻辑最多包含 1024 个网络"); | |||
| setError(error, "单组控制逻辑最多包含 256 个网络"); | |||
| return false; | |||
| } | |||
| if (hasDuplicateId(rungs)) | |||
| @@ -126,7 +126,7 @@ bool HmiControl::validate(std::string *error) const | |||
| } | |||
| if (text.size() > ProjectLimits::kMaximumTextBytes) | |||
| { | |||
| setError(error, "HMI 控件文本不能超过 4096 个 UTF-8 字节"); | |||
| setError(error, "HMI 控件文本不能超过 256 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| if (bounds.width <= 0 || bounds.height <= 0 | |||
| @@ -155,7 +155,7 @@ bool HmiControl::validate(std::string *error) const | |||
| } | |||
| if (property.second.size() > ProjectLimits::kMaximumPropertyValueBytes) | |||
| { | |||
| setError(error, "HMI 控件属性值不能超过 4096 个 UTF-8 字节"); | |||
| setError(error, "HMI 控件属性值不能超过 1024 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| if (!validateAppearanceProperty(property.first, property.second, error)) | |||
| @@ -268,7 +268,7 @@ bool HmiPage::validate(std::string *error) const | |||
| } | |||
| if (name.size() > ProjectLimits::kMaximumTextBytes) | |||
| { | |||
| setError(error, "HMI 页面名称不能超过 4096 个 UTF-8 字节"); | |||
| setError(error, "HMI 页面名称不能超过 256 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| if (width < ProjectLimits::kMinimumHmiPageWidth | |||
| @@ -283,7 +283,7 @@ bool HmiPage::validate(std::string *error) const | |||
| } | |||
| if (controls.size() > ProjectLimits::kMaximumHmiControlsPerPage) | |||
| { | |||
| setError(error, "单个 HMI 页面最多包含 512 个控件"); | |||
| setError(error, "单个 HMI 页面最多包含 128 个控件"); | |||
| return false; | |||
| } | |||
| @@ -7,30 +7,32 @@ namespace ProjectLimits { | |||
| constexpr std::size_t kMaximumProjectFileBytes = 16U * 1024U * 1024U; // 一个工程文件最大 16 MiB,防止异常大文件占满内存 | |||
| constexpr std::size_t kMaximumHmiPages = 128U; // 一个工程最多放 128 个 HMI 页面 | |||
| constexpr std::size_t kMaximumHmiControlsPerPage = 512U; // 一个 HMI 页面最多放 512 个控件 | |||
| constexpr std::size_t kMaximumAlarmDefinitions = 512U; // 一个工程最多配置 512 条报警 | |||
| constexpr std::size_t kMaximumHmiPages = 32U; // 一个工程最多放 32 个 HMI 页面 | |||
| constexpr std::size_t kMaximumHmiControlsPerPage = 128U; // 一个 HMI 页面最多放 128 个控件 | |||
| constexpr std::size_t kMaximumHmiControlsPerProject = 2048U; // 一个工程最多保存 2048 个 HMI 控件 | |||
| constexpr std::size_t kMaximumAlarmDefinitions = 256U; // 一个工程最多配置 256 条报警 | |||
| constexpr std::size_t kMaximumRegisterComments = 8002U; // M0~M4000 和 D0~D4000 最多各写一条注释 | |||
| constexpr std::size_t kMaximumControlLogics = 128U; // 一个工程最多放 128 组梯形图控制逻辑 | |||
| constexpr std::size_t kMaximumRungsPerLogic = 1024U; // 一组控制逻辑最多放 1024 个网络 | |||
| constexpr std::size_t kMaximumControlLogics = 32U; // 一个工程最多放 32 组梯形图控制逻辑 | |||
| constexpr std::size_t kMaximumRungsPerLogic = 256U; // 一组控制逻辑最多放 256 个网络 | |||
| constexpr std::size_t kMaximumRungsPerProject = 2048U; // 一个工程最多保存 2048 个网络 | |||
| constexpr std::size_t kMaximumExpressionNodesPerRung = 4096U; // 一个网络里的触点、横线和内部结构最多共 4096 个 | |||
| constexpr std::size_t kMaximumExpressionDepth = 20U; // 并联套并联时最多套 20 层,防止结构无限变复杂 | |||
| constexpr std::size_t kMaximumExpressionChildren = 256U; // 一个串联组或并联组最多放 256 个子项 | |||
| constexpr std::size_t kMaximumExpressionNodesPerRung = 1024U; // 一个网络里的触点、横线和内部结构最多共 1024 个 | |||
| constexpr std::size_t kMaximumExpressionDepth = 12U; // 并联套并联时最多套 12 层,防止结构无限变复杂 | |||
| constexpr std::size_t kMaximumExpressionChildren = 64U; // 一个串联组或并联组最多放 64 个子项 | |||
| constexpr int kMaximumConditionColumns = 10; // 一个网络前面的条件区最多 10 列 | |||
| constexpr int kMaximumLadderColumns = 11; // 一个网络总共 11 列,最后 1 列专门放输出 | |||
| constexpr int kMaximumLogicRows = 256; // 一个网络上下最多 256 行并联支路 | |||
| constexpr int kMaximumLogicRows = 64; // 一个网络上下最多 64 行并联支路 | |||
| static_assert(kMaximumLadderColumns == kMaximumConditionColumns + 1); // 确保总列数始终等于条件列加输出列 | |||
| constexpr std::size_t kMaximumHmiProperties = 64U; // 一个 HMI 控件最多保存 64 对扩展属性 | |||
| constexpr std::size_t kMaximumIdBytes = 128U; // ID 最多 128 个 UTF-8 字节,够用来做稳定标识 | |||
| constexpr std::size_t kMaximumTextBytes = 4096U; // 工程名称等普通文本最多 4096 个 UTF-8 字节 | |||
| constexpr std::size_t kMaximumTextBytes = 256U; // 工程名称等普通文本最多 256 个 UTF-8 字节 | |||
| constexpr std::size_t kMaximumHmiControlTextCharacters = 12U; // HMI 控件显示文字最多输入 12 个字符 | |||
| constexpr std::size_t kMaximumAlarmTitleCharacters = 12U; // 报警列表标题最多输入 12 个字符 | |||
| constexpr std::size_t kMaximumAlarmMessageCharacters = 20U; // 一条报警内容最多输入 20 个字符 | |||
| constexpr std::size_t kMaximumPropertyKeyBytes = 128U; // HMI 属性名最多 128 个 UTF-8 字节 | |||
| constexpr std::size_t kMaximumPropertyValueBytes = 4096U; // HMI 属性值最多 4096 个 UTF-8 字节 | |||
| constexpr std::size_t kMaximumPropertyValueBytes = 1024U; // HMI 属性值最多 1024 个 UTF-8 字节 | |||
| constexpr std::size_t kMaximumRegisterCommentBytes = 64U; // M/D 注释最多 64 个 UTF-8 字节,保证节点下方放得下 | |||
| constexpr std::size_t kMaximumRungCommentBytes = 128U; // 网络注释最多 128 个 UTF-8 字节,并且保持单行显示 | |||
| @@ -47,8 +49,8 @@ constexpr int kMaximumHmiFontPointSize = 72; // HMI 字体最大 72 磅,防止 | |||
| constexpr int kAlarmDefaultFontPointReduction = 2; // 报警列表默认比普通文字小 2 磅,给表头留空间 | |||
| constexpr int kMaximumVisibleAlarmRows = 5; // 报警列表默认最多同时显示 5 行,更多内容通过翻页查看 | |||
| constexpr std::size_t kMaximumPollAddresses = 1024U; // PLC 一轮轮询最多读取 1024 个去重后的地址 | |||
| constexpr std::size_t kMaximumPollBlocks = 64U; // PLC 一轮轮询最多拆成 64 个读块 | |||
| constexpr std::size_t kMaximumPollAddresses = 256U; // PLC 一轮轮询最多读取 256 个去重后的地址 | |||
| constexpr std::size_t kMaximumPollBlocks = 8U; // PLC 一轮轮询最多拆成 8 个读块 | |||
| constexpr int kMaximumModbusReadCount = 120; // 一条 Modbus 读指令最多读 120 个数据项 | |||
| constexpr int kMinimumPlcServerAddress = 1; // Modbus 站号从 1 开始 | |||
| @@ -125,7 +125,7 @@ bool Project::validate(std::string *error) const | |||
| } | |||
| if (metadata.name.size() > ProjectLimits::kMaximumTextBytes) | |||
| { | |||
| setError(error, "工程名称不能超过 4096 个 UTF-8 字节"); | |||
| setError(error, "工程名称不能超过 256 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| if (initialHmiPageId.size() > ProjectLimits::kMaximumIdBytes) | |||
| @@ -135,12 +135,12 @@ bool Project::validate(std::string *error) const | |||
| } | |||
| if (hmiPages.size() > ProjectLimits::kMaximumHmiPages) | |||
| { | |||
| setError(error, "单个工程最多包含 128 个 HMI 页面"); | |||
| setError(error, "单个工程最多包含 32 个 HMI 页面"); | |||
| return false; | |||
| } | |||
| if (alarmDefinitions.size() > ProjectLimits::kMaximumAlarmDefinitions) | |||
| { | |||
| setError(error, "单个工程最多包含 512 条报警定义"); | |||
| setError(error, "单个工程最多包含 256 条报警定义"); | |||
| return false; | |||
| } | |||
| if (registerComments.size() > ProjectLimits::kMaximumRegisterComments) | |||
| @@ -150,7 +150,27 @@ bool Project::validate(std::string *error) const | |||
| } | |||
| if (controlLogics.size() > ProjectLimits::kMaximumControlLogics) | |||
| { | |||
| setError(error, "单个工程最多包含 128 组控制逻辑"); | |||
| setError(error, "单个工程最多包含 32 组控制逻辑"); | |||
| return false; | |||
| } | |||
| std::size_t total_hmi_controls = 0U; | |||
| for (const HmiPage &page : hmiPages) | |||
| { | |||
| total_hmi_controls += page.controls.size(); | |||
| } | |||
| if (total_hmi_controls > ProjectLimits::kMaximumHmiControlsPerProject) | |||
| { | |||
| setError(error, "单个工程最多包含 2048 个 HMI 控件"); | |||
| return false; | |||
| } | |||
| std::size_t total_rungs = 0U; | |||
| for (const ControlLogic &logic : controlLogics) | |||
| { | |||
| total_rungs += logic.rungs.size(); | |||
| } | |||
| if (total_rungs > ProjectLimits::kMaximumRungsPerProject) | |||
| { | |||
| setError(error, "单个工程最多包含 2048 个梯形图网络"); | |||
| return false; | |||
| } | |||
| if (containsDuplicateId(hmiPages)) | |||
| @@ -307,7 +327,7 @@ bool Project::validate(std::string *error) const | |||
| poll_addresses.end()); | |||
| if (poll_addresses.size() > ProjectLimits::kMaximumPollAddresses) | |||
| { | |||
| setError(error, "工程中需要 PLC 轮询的去重 M/D 地址最多为 1024 个"); | |||
| setError(error, "工程中需要 PLC 轮询的去重 M/D 地址最多为 256 个"); | |||
| return false; | |||
| } | |||
| return true; | |||
| @@ -631,7 +631,7 @@ bool parseProperties( | |||
| { | |||
| return state->fail( | |||
| ProjectStorageError::InvalidField, | |||
| "HMI 控件属性名称最多 128 字节,属性值最多 4096 字节"); | |||
| "HMI 控件属性名称最多 128 字节,属性值最多 1024 字节"); | |||
| } | |||
| properties->emplace(key, value); | |||
| } | |||
| @@ -1353,14 +1353,14 @@ bool parseConditionExpression( | |||
| { | |||
| return state->fail( | |||
| ProjectStorageError::InvalidField, | |||
| context + " 的嵌套深度超过 20 层"); | |||
| context + " 的嵌套深度超过 12 层"); | |||
| } | |||
| ++*node_count; | |||
| if (*node_count > ProjectLimits::kMaximumExpressionNodesPerRung) | |||
| { | |||
| return state->fail( | |||
| ProjectStorageError::InvalidField, | |||
| context + " 的表达式节点总数超过 4096 个"); | |||
| context + " 的表达式节点总数超过 1024 个"); | |||
| } | |||
| std::string kind; | |||
| if (!readString( | |||
| @@ -72,7 +72,7 @@ bool normalizePollAddresses( | |||
| std::unique(normalized->begin(), normalized->end()), normalized->end()); | |||
| if (normalized->size() > ProjectLimits::kMaximumPollAddresses) | |||
| { | |||
| *error = "PLC 轮询的去重 M/D 地址最多为 1024 个"; | |||
| *error = "PLC 轮询的去重 M/D 地址最多为 256 个"; | |||
| return false; | |||
| } | |||
| return true; | |||
| @@ -269,7 +269,7 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses( | |||
| } | |||
| if (pollBlockCount(normalized) > ProjectLimits::kMaximumPollBlocks) | |||
| { | |||
| return {false, "PLC 轮询地址拆分后最多允许 64 个读块"}; | |||
| return {false, "PLC 轮询地址拆分后最多允许 8 个读块"}; | |||
| } | |||
| // 当前读请求完成前暂存新集合,避免按半旧半新的地址解析回复 | |||
| if (pending_reply_ != nullptr) | |||
| @@ -39,7 +39,7 @@ AlarmEditorResult AlarmEditorService::addDefinition( | |||
| { | |||
| if (definitions().size() >= ProjectLimits::kMaximumAlarmDefinitions) | |||
| { | |||
| return failure(AlarmEditorError::InvalidDefinition, "单个工程最多包含 512 条报警定义"); | |||
| return failure(AlarmEditorError::InvalidDefinition, "单个工程最多包含 256 条报警定义"); | |||
| } | |||
| AlarmDefinition candidate = definition; | |||
| candidate.id = makeUniqueId(); | |||
| @@ -16,8 +16,8 @@ template <typename State> | |||
| class EditorHistory final | |||
| { | |||
| public: | |||
| // 单个编辑服务最多保留 100 条撤销和重做历史 | |||
| static constexpr std::size_t kMaximumEntries = 100U; | |||
| // 单个编辑服务最多保留 30 条撤销和重做历史 | |||
| static constexpr std::size_t kMaximumEntries = 30U; | |||
| template <typename Equal> | |||
| /** | |||
| @@ -51,6 +51,16 @@ std::string makeUniquePageId(const Project &project) | |||
| } | |||
| } | |||
| std::size_t totalControlCount(const Project &project) | |||
| { | |||
| std::size_t total = 0U; | |||
| for (const HmiPage &page : project.hmiPages) | |||
| { | |||
| total += page.controls.size(); | |||
| } | |||
| return total; | |||
| } | |||
| } // namespace | |||
| HmiEditorService::HmiEditorService(ProjectService &project_service) | |||
| @@ -213,11 +223,11 @@ HmiEditorResult HmiEditorService::addPage(const std::string &name) | |||
| const Project ¤t = project_service_.project(); | |||
| if (current.hmiPages.size() >= ProjectLimits::kMaximumHmiPages) | |||
| { | |||
| return failure(HmiEditorError::InvalidPage, "单个工程最多包含 128 个 HMI 页面"); | |||
| return failure(HmiEditorError::InvalidPage, "单个工程最多包含 32 个 HMI 页面"); | |||
| } | |||
| if (name.size() > ProjectLimits::kMaximumTextBytes) | |||
| { | |||
| return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 4096 个 UTF-8 字节"); | |||
| return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 256 个 UTF-8 字节"); | |||
| } | |||
| const bool duplicate = std::any_of( | |||
| current.hmiPages.cbegin(), current.hmiPages.cend(), | |||
| @@ -284,7 +294,7 @@ HmiEditorResult HmiEditorService::renamePage( | |||
| } | |||
| if (name.size() > ProjectLimits::kMaximumTextBytes) | |||
| { | |||
| return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 4096 个 UTF-8 字节"); | |||
| return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 256 个 UTF-8 字节"); | |||
| } | |||
| const Project ¤t = project_service_.project(); | |||
| const auto existing = std::find_if( | |||
| @@ -425,7 +435,12 @@ HmiEditorResult HmiEditorService::addControl( | |||
| } | |||
| if (page->controls.size() >= ProjectLimits::kMaximumHmiControlsPerPage) | |||
| { | |||
| return failure(HmiEditorError::InvalidControl, "单个 HMI 页面最多包含 512 个控件"); | |||
| return failure(HmiEditorError::InvalidControl, "单个 HMI 页面最多包含 128 个控件"); | |||
| } | |||
| if (totalControlCount(project_service_.project()) | |||
| >= ProjectLimits::kMaximumHmiControlsPerProject) | |||
| { | |||
| return failure(HmiEditorError::InvalidControl, "单个工程最多包含 2048 个 HMI 控件"); | |||
| } | |||
| const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); | |||
| if (descriptor == nullptr) | |||
| @@ -620,7 +635,12 @@ HmiEditorResult HmiEditorService::pasteControls( | |||
| || existing_page->controls.size() + controls.size() | |||
| > ProjectLimits::kMaximumHmiControlsPerPage) | |||
| { | |||
| return failure(HmiEditorError::InvalidControl, "单个 HMI 页面最多包含 512 个控件"); | |||
| return failure(HmiEditorError::InvalidControl, "单个 HMI 页面最多包含 128 个控件"); | |||
| } | |||
| if (totalControlCount(project_service_.project()) + controls.size() | |||
| > ProjectLimits::kMaximumHmiControlsPerProject) | |||
| { | |||
| return failure(HmiEditorError::InvalidControl, "单个工程最多包含 2048 个 HMI 控件"); | |||
| } | |||
| // 让整组控件尽量保留相对位置,靠近页面边界时自动向页面内收拢 | |||
| @@ -45,6 +45,25 @@ std::string makeUniqueLogicId(const Project &project) | |||
| } | |||
| } | |||
| std::size_t totalRungCount(const Project &project) | |||
| { | |||
| std::size_t total = 0U; | |||
| for (const ControlLogic &logic : project.controlLogics) | |||
| { | |||
| total += logic.rungs.size(); | |||
| } | |||
| return total; | |||
| } | |||
| std::string rungLimitMessage(const Project &project, const ControlLogic &logic) | |||
| { | |||
| return logic.rungs.size() >= ProjectLimits::kMaximumRungsPerLogic | |||
| ? "单组控制逻辑最多包含 256 个网络" | |||
| : totalRungCount(project) >= ProjectLimits::kMaximumRungsPerProject | |||
| ? "单个工程最多包含 2048 个梯形图网络" | |||
| : std::string{}; | |||
| } | |||
| LogicNode makeNode(const std::string &id, const LogicNodeConfig &config) | |||
| { | |||
| return {id, config, false}; | |||
| @@ -897,11 +916,11 @@ LogicEditorResult LogicEditorService::addLogic(const std::string &name) | |||
| const Project ¤t = project_service_.project(); | |||
| if (current.controlLogics.size() >= ProjectLimits::kMaximumControlLogics) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "单个工程最多包含 128 组控制逻辑"); | |||
| return failure(LogicEditorError::InvalidOperation, "单个工程最多包含 32 组控制逻辑"); | |||
| } | |||
| if (name.size() > ProjectLimits::kMaximumTextBytes) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能超过 4096 个 UTF-8 字节"); | |||
| return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能超过 256 个 UTF-8 字节"); | |||
| } | |||
| const bool duplicate = std::any_of( | |||
| current.controlLogics.cbegin(), current.controlLogics.cend(), | |||
| @@ -930,7 +949,7 @@ LogicEditorResult LogicEditorService::renameLogic( | |||
| } | |||
| if (name.size() > ProjectLimits::kMaximumTextBytes) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能超过 4096 个 UTF-8 字节"); | |||
| return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能超过 256 个 UTF-8 字节"); | |||
| } | |||
| const Project ¤t = project_service_.project(); | |||
| const auto logic = std::find_if( | |||
| @@ -1050,9 +1069,11 @@ LogicEditorResult LogicEditorService::addRung(const std::string &logic_id) | |||
| { | |||
| return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"); | |||
| } | |||
| if (logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) | |||
| const std::string limit_message = rungLimitMessage( | |||
| project_service_.project(), *logic); | |||
| if (!limit_message.empty()) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); | |||
| return failure(LogicEditorError::InvalidOperation, limit_message); | |||
| } | |||
| LadderRung rung; | |||
| rung.id = makeUniqueRungId(*logic); | |||
| @@ -1142,10 +1163,11 @@ LogicEditorResult LogicEditorService::appendCondition( | |||
| { | |||
| return failure(LogicEditorError::RungNotFound, "未找到梯形图网络"); | |||
| } | |||
| if (create_rung | |||
| && logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) | |||
| const std::string limit_message = create_rung | |||
| ? rungLimitMessage(project_service_.project(), *logic) : std::string{}; | |||
| if (!limit_message.empty()) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); | |||
| return failure(LogicEditorError::InvalidOperation, limit_message); | |||
| } | |||
| if (!isConditionConfig(config)) | |||
| { | |||
| @@ -1416,10 +1438,11 @@ LogicEditorResult LogicEditorService::appendWire( | |||
| // 无选中目标的连续追加达到十列后,原子切换到下一个网络 | |||
| create_rung = true; | |||
| } | |||
| if (create_rung | |||
| && logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) | |||
| const std::string limit_message = create_rung | |||
| ? rungLimitMessage(project_service_.project(), *logic) : std::string{}; | |||
| if (!limit_message.empty()) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); | |||
| return failure(LogicEditorError::InvalidOperation, limit_message); | |||
| } | |||
| const std::string target_rung_id = create_rung | |||
| ? makeUniqueRungId(*logic) : rung_id; | |||
| @@ -2149,10 +2172,11 @@ LogicEditorResult LogicEditorService::setOutput( | |||
| ? "未找到梯形图网络" | |||
| : "梯形图输出必须使用有效的输出指令"); | |||
| } | |||
| if (rung_id.empty() | |||
| && logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) | |||
| const std::string limit_message = rung_id.empty() | |||
| ? rungLimitMessage(project_service_.project(), *logic) : std::string{}; | |||
| if (!limit_message.empty()) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); | |||
| return failure(LogicEditorError::InvalidOperation, limit_message); | |||
| } | |||
| const bool create_rung = rung_id.empty(); | |||
| const std::string target_rung_id = create_rung | |||
| @@ -2282,10 +2306,12 @@ LogicEditorResult LogicEditorService::pasteConditionNodes( | |||
| { | |||
| return failure(LogicEditorError::RungNotFound, "未找到梯形图网络"); | |||
| } | |||
| if (rung_id.empty() | |||
| && existing_logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) | |||
| const std::string limit_message = rung_id.empty() | |||
| ? rungLimitMessage(project_service_.project(), *existing_logic) | |||
| : std::string{}; | |||
| if (!limit_message.empty()) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); | |||
| return failure(LogicEditorError::InvalidOperation, limit_message); | |||
| } | |||
| const HistoryState before = captureState(); | |||
| @@ -2406,9 +2432,11 @@ LogicEditorResult LogicEditorService::pasteRung( | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, source_error); | |||
| } | |||
| if (existing_logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) | |||
| const std::string limit_message = rungLimitMessage( | |||
| project_service_.project(), *existing_logic); | |||
| if (!limit_message.empty()) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); | |||
| return failure(LogicEditorError::InvalidOperation, limit_message); | |||
| } | |||
| const HistoryState before = captureState(); | |||
| @@ -271,7 +271,7 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses() | |||
| addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end()); | |||
| if (addresses.size() > ProjectLimits::kMaximumPollAddresses) | |||
| { | |||
| return {false, "PLC 轮询的去重 M/D 地址最多为 1024 个"}; | |||
| return {false, "PLC 轮询的去重 M/D 地址最多为 256 个"}; | |||
| } | |||
| return plc_gateway_->setPollAddresses(addresses); | |||
| } | |||
| @@ -356,10 +356,38 @@ void testQuantityBoundaries() | |||
| 400, | |||
| {}}); | |||
| } | |||
| require(project.validate(), "an HMI page count of 128 must be accepted"); | |||
| require(project.validate(), "an HMI page count at the configured limit must be accepted"); | |||
| project.hmiPages.push_back({"page-over", "Page over", 800, 400, {}}); | |||
| require(!project.validate(), "an HMI page count of 129 must be rejected"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.clear(); | |||
| project.initialHmiPageId.clear(); | |||
| for (std::size_t page_index = 0U; page_index < 17U; ++page_index) | |||
| { | |||
| HmiPage page{ | |||
| "bulk-page-" + std::to_string(page_index), | |||
| "Bulk page " + std::to_string(page_index), | |||
| 800, | |||
| 400, | |||
| {}}; | |||
| for (std::size_t control_index = 0U; | |||
| control_index < ProjectLimits::kMaximumHmiControlsPerPage; | |||
| ++control_index) | |||
| { | |||
| HmiControl label; | |||
| label.id = "label-" + std::to_string(control_index); | |||
| label.type = HmiControlType::Label; | |||
| label.bounds = {0, 0, 1, 1}; | |||
| label.text = "label"; | |||
| page.controls.push_back(std::move(label)); | |||
| } | |||
| project.hmiPages.push_back(std::move(page)); | |||
| } | |||
| project.initialHmiPageId = project.hmiPages.front().id; | |||
| require(!project.validate(), | |||
| "an HMI control count over the project limit must be rejected"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.front().controls.clear(); | |||
| for (std::size_t index = 0U; | |||
| @@ -373,7 +401,7 @@ void testQuantityBoundaries() | |||
| label.text = "label"; | |||
| project.hmiPages.front().controls.push_back(std::move(label)); | |||
| } | |||
| require(project.validate(), "a page control count of 512 must be accepted"); | |||
| require(project.validate(), "a page control count at the configured limit must be accepted"); | |||
| HmiControl extra_label; | |||
| extra_label.id = "label-over"; | |||
| extra_label.type = HmiControlType::Label; | |||
| @@ -382,6 +410,31 @@ void testQuantityBoundaries() | |||
| project.hmiPages.front().controls.push_back(std::move(extra_label)); | |||
| require(!project.validate(), "a page control count of 513 must be rejected"); | |||
| project = makeValidProject(); | |||
| project.controlLogics.clear(); | |||
| for (std::size_t logic_index = 0U; logic_index < 9U; ++logic_index) | |||
| { | |||
| ControlLogic logic{ | |||
| "bulk-logic-" + std::to_string(logic_index), | |||
| "Bulk logic " + std::to_string(logic_index), | |||
| {}, | |||
| true}; | |||
| for (std::size_t rung_index = 0U; | |||
| rung_index < ProjectLimits::kMaximumRungsPerLogic; | |||
| ++rung_index) | |||
| { | |||
| logic.rungs.push_back({ | |||
| "rung-" + std::to_string(rung_index), | |||
| "Rung " + std::to_string(rung_index), | |||
| {}, | |||
| std::nullopt, | |||
| {}}); | |||
| } | |||
| project.controlLogics.push_back(std::move(logic)); | |||
| } | |||
| require(!project.validate(), | |||
| "a ladder rung count over the project limit must be rejected"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth; | |||
| project.hmiPages.front().height = ProjectLimits::kMaximumHmiPageHeight; | |||
| @@ -427,12 +480,12 @@ void testQuantityBoundaries() | |||
| ConditionExpression maximum_depth = makeNested( | |||
| static_cast<int>(ProjectLimits::kMaximumExpressionDepth), &next_address); | |||
| require(maximum_depth.validate(), | |||
| "an expression depth of 20 must be accepted"); | |||
| "an expression depth at the configured limit must be accepted"); | |||
| ConditionExpression excessive_depth = makeNested( | |||
| static_cast<int>(ProjectLimits::kMaximumExpressionDepth) + 1, | |||
| &next_address); | |||
| require(!excessive_depth.validate(), | |||
| "an expression depth of 21 must be rejected"); | |||
| "an expression depth above the configured limit must be rejected"); | |||
| (void)leaf; | |||
| } | |||
| @@ -2,6 +2,7 @@ | |||
| #include "domain/register_repository.h" | |||
| #include "domain/virtual_register_repository.h" | |||
| #include "services/hmi_editor_service.h" | |||
| #include "services/editor_history.h" | |||
| #include "services/hmi_runtime_service.h" | |||
| #include "services/hmi_navigation_service.h" | |||
| #include "services/project_service.h" | |||
| @@ -218,8 +219,8 @@ void testHistoryAndAtomicBatchDelete() | |||
| { | |||
| ++undo_count; | |||
| } | |||
| require(undo_count == 100, | |||
| "HMI history must retain exactly the configured 100 most recent steps"); | |||
| require(undo_count == static_cast<int>(EditorHistory<int>::kMaximumEntries), | |||
| "HMI history must retain exactly the configured most recent steps"); | |||
| } | |||
| void testBatchPasteControls() | |||
| @@ -262,6 +263,82 @@ void testBatchPasteControls() | |||
| "batch control paste must be one undoable operation"); | |||
| } | |||
| void testRuntimeValidationAndPasteBoundaries() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| HmiEditorService editor(project_service); | |||
| const std::string page_id = editor.ensureDefaultPage().id; | |||
| HmiControl label; | |||
| label.id = "label-source"; | |||
| label.type = HmiControlType::Label; | |||
| label.bounds = {760, 360, 32, 32}; | |||
| label.text = "边界"; | |||
| require(editor.pasteControls(page_id, {label}, 20, 20).succeeded, | |||
| "a paste near the page edge must be clamped into the page"); | |||
| const HmiPage *page = editor.findPage(page_id); | |||
| require(page != nullptr && page->controls.size() == 1U, | |||
| "edge paste must add exactly one control"); | |||
| const HmiControl &edge_copy = page->controls.front(); | |||
| require(edge_copy.bounds.x + edge_copy.bounds.width <= page->width | |||
| && edge_copy.bounds.y + edge_copy.bounds.height <= page->height | |||
| && edge_copy.bounds.x >= 0 && edge_copy.bounds.y >= 0, | |||
| "edge paste must keep the copied control inside page bounds"); | |||
| require(!editor.pasteControls(page_id, {}).succeeded, | |||
| "an empty HMI clipboard must be rejected"); | |||
| HmiControl oversized = label; | |||
| oversized.bounds = {0, 0, page->width + 1, 32}; | |||
| require(!editor.pasteControls(page_id, {oversized}).succeeded | |||
| && editor.findPage(page_id)->controls.size() == 1U, | |||
| "a copied control wider than the target page must fail atomically"); | |||
| HmiControl unsupported = label; | |||
| unsupported.type = HmiControlType::Count; | |||
| require(!editor.pasteControls(page_id, {unsupported}).succeeded | |||
| && editor.findPage(page_id)->controls.size() == 1U, | |||
| "an unsupported copied HMI type must fail atomically"); | |||
| std::vector<HmiControl> too_many(513U, label); | |||
| require(!editor.pasteControls(page_id, too_many).succeeded | |||
| && editor.findPage(page_id)->controls.size() == 1U, | |||
| "a paste batch above the page limit must be rejected before mutation"); | |||
| VirtualRegisterRepository repository; | |||
| HmiRuntimeService runtime(repository); | |||
| HmiControl unbound_indicator; | |||
| unbound_indicator.type = HmiControlType::Indicator; | |||
| require(runtime.readControl(unbound_indicator).error | |||
| == HmiRuntimeError::MissingBinding, | |||
| "an unbound runtime indicator must report MissingBinding"); | |||
| HmiControl wrong_area_indicator = unbound_indicator; | |||
| wrong_area_indicator.binding = RegisterAddress{RegisterArea::D, 0}; | |||
| require(runtime.readControl(wrong_area_indicator).error | |||
| == HmiRuntimeError::InvalidBinding, | |||
| "an indicator bound to D must report InvalidBinding"); | |||
| HmiControl invalid_indicator = unbound_indicator; | |||
| invalid_indicator.binding = RegisterAddress{RegisterArea::M, 4001}; | |||
| require(runtime.readControl(invalid_indicator).error | |||
| == HmiRuntimeError::InvalidBinding, | |||
| "an indicator with an out-of-range address must report InvalidBinding"); | |||
| require(runtime.readControl(label).error | |||
| == HmiRuntimeError::UnsupportedControl, | |||
| "a static label must not be treated as a register runtime control"); | |||
| HmiControl wrong_button = unbound_indicator; | |||
| wrong_button.type = HmiControlType::Button; | |||
| wrong_button.binding = RegisterAddress{RegisterArea::D, 0}; | |||
| require(runtime.operateButton(wrong_button, HmiButtonEvent::Pressed).error | |||
| == HmiRuntimeError::InvalidBinding, | |||
| "a button bound to D must reject runtime writes"); | |||
| HmiControl wrong_numeric = unbound_indicator; | |||
| wrong_numeric.type = HmiControlType::NumericInput; | |||
| wrong_numeric.binding = RegisterAddress{RegisterArea::M, 0}; | |||
| require(runtime.writeNumericInput(wrong_numeric, 1).error | |||
| == HmiRuntimeError::InvalidBinding, | |||
| "a numeric input bound to M must reject runtime writes"); | |||
| } | |||
| void testAppearanceEditing() | |||
| { | |||
| TestProjectStorage storage; | |||
| @@ -417,6 +494,7 @@ int main() | |||
| testControlEditing(); | |||
| testHistoryAndAtomicBatchDelete(); | |||
| testBatchPasteControls(); | |||
| testRuntimeValidationAndPasteBoundaries(); | |||
| testAppearanceEditing(); | |||
| testRuntimeUsesRegisterRepository(); | |||
| testPageLifecycleAndNavigation(); | |||
| @@ -1,6 +1,7 @@ | |||
| #include "domain/project_storage.h" | |||
| #include "domain/project_limits.h" | |||
| #include "services/logic_editor_service.h" | |||
| #include "services/editor_history.h" | |||
| #include "services/project_service.h" | |||
| #include "support/test_support.h" | |||
| @@ -1003,8 +1004,8 @@ void testHistoryAndAtomicBatchDelete() | |||
| { | |||
| ++undo_count; | |||
| } | |||
| require(undo_count == 100, | |||
| "logic history must retain exactly the configured 100 most recent steps"); | |||
| require(undo_count == static_cast<int>(EditorHistory<int>::kMaximumEntries), | |||
| "logic history must retain exactly the configured most recent steps"); | |||
| require(service.redo().succeeded, | |||
| "logic redo must be available after an undo"); | |||
| @@ -1110,6 +1111,126 @@ void testBatchPasteNodesAndRung() | |||
| "whole rung paste must regenerate ids and preserve output configuration"); | |||
| } | |||
| void testConditionPasteTargetsAndValidation() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| LogicNode source; | |||
| source.id = "copied-condition"; | |||
| source.config = contact(90); | |||
| source.configured = true; | |||
| const std::string empty_column_rung = makeEmptyRung(service, logic_id); | |||
| LogicConditionPasteTarget empty_column; | |||
| empty_column.kind = LogicConditionPasteTargetKind::EmptyColumn; | |||
| empty_column.column = 2; | |||
| require(service.pasteConditionNodes( | |||
| logic_id, empty_column_rung, {source}, empty_column).succeeded, | |||
| "condition paste must support an empty grid column"); | |||
| require(conditionColumns(*service.findRung( | |||
| logic_id, empty_column_rung)->condition) == 3, | |||
| "empty-column paste must preserve the requested column offset"); | |||
| const std::string branch_rung = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult branch_source = service.appendCondition( | |||
| logic_id, branch_rung, contact(1)); | |||
| const LogicEditorResult branch_source_two = service.appendCondition( | |||
| logic_id, branch_rung, contact(2)); | |||
| const LogicEditorResult branch_source_three = service.appendCondition( | |||
| logic_id, branch_rung, contact(3)); | |||
| require(branch_source.succeeded && branch_source_two.succeeded | |||
| && branch_source_three.succeeded, | |||
| "branch paste setup must add a source range"); | |||
| const LogicEditorResult branch = service.addParallelBranch( | |||
| logic_id, branch_rung, | |||
| {branch_source.id, branch_source_two.id, branch_source_three.id}, | |||
| contact(4)); | |||
| require(branch.succeeded, "branch paste setup must create a parallel branch"); | |||
| LogicConditionPasteTarget branch_target; | |||
| branch_target.kind = LogicConditionPasteTargetKind::BranchEmptyColumn; | |||
| branch_target.expressionId = branch.id; | |||
| branch_target.column = 2; | |||
| require(service.pasteConditionNodes( | |||
| logic_id, branch_rung, {source}, branch_target).succeeded, | |||
| "condition paste must support a visible empty cell in a branch"); | |||
| const std::string after_node_rung = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult after_source = service.appendCondition( | |||
| logic_id, after_node_rung, contact(3)); | |||
| require(after_source.succeeded, "after-node paste setup must add a source node"); | |||
| LogicConditionPasteTarget after_target; | |||
| after_target.kind = LogicConditionPasteTargetKind::AfterNode; | |||
| after_target.expressionId = after_source.id; | |||
| require(service.pasteConditionNodes( | |||
| logic_id, after_node_rung, {source}, after_target).succeeded, | |||
| "condition paste must support insertion after a selected node"); | |||
| require(conditionNodes(*service.findRung( | |||
| logic_id, after_node_rung)->condition) == 2, | |||
| "after-node paste must add exactly one condition"); | |||
| const std::string replace_wire_rung = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult wire = service.appendWire( | |||
| logic_id, replace_wire_rung, 2); | |||
| require(wire.succeeded, "replace-wire paste setup must add a wire"); | |||
| LogicConditionPasteTarget replace_wire; | |||
| replace_wire.kind = LogicConditionPasteTargetKind::ReplaceWire; | |||
| replace_wire.expressionId = wire.id; | |||
| require(service.pasteConditionNodes( | |||
| logic_id, replace_wire_rung, {source}, replace_wire).succeeded, | |||
| "condition paste must replace an entire wire expression"); | |||
| require(conditionNodes(*service.findRung( | |||
| logic_id, replace_wire_rung)->condition) == 1, | |||
| "whole-wire paste must create one condition node"); | |||
| const std::string replace_cell_rung = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult cell_wire = service.appendWire( | |||
| logic_id, replace_cell_rung, 3); | |||
| require(cell_wire.succeeded, "replace-cell paste setup must add a wire"); | |||
| LogicConditionPasteTarget replace_cell; | |||
| replace_cell.kind = LogicConditionPasteTargetKind::ReplaceWireColumn; | |||
| replace_cell.expressionId = cell_wire.id; | |||
| replace_cell.column = 1; | |||
| require(service.pasteConditionNodes( | |||
| logic_id, replace_cell_rung, {source}, replace_cell).succeeded, | |||
| "condition paste must replace one selected wire cell"); | |||
| require(conditionColumns(*service.findRung( | |||
| logic_id, replace_cell_rung)->condition) == 3, | |||
| "wire-cell paste must preserve the original network width"); | |||
| require(!service.pasteConditionNodes(logic_id, after_node_rung, {}).succeeded, | |||
| "an empty condition clipboard must be rejected"); | |||
| LogicNode output = source; | |||
| output.config = CoilNodeConfig{RegisterAddress{RegisterArea::M, 91}, CoilMode::Normal}; | |||
| require(!service.pasteConditionNodes( | |||
| logic_id, after_node_rung, {output}).succeeded, | |||
| "an output instruction must not be pasted into the condition area"); | |||
| require(!service.pasteConditionNodes( | |||
| "missing-logic", after_node_rung, {source}).succeeded, | |||
| "condition paste must reject an unknown logic"); | |||
| require(!service.pasteConditionNodes( | |||
| logic_id, "missing-rung", {source}).succeeded, | |||
| "condition paste must reject an unknown rung"); | |||
| const LadderRung *before_invalid = service.findRung( | |||
| logic_id, after_node_rung); | |||
| require(before_invalid != nullptr && before_invalid->condition.has_value(), | |||
| "invalid paste setup must retain its target network"); | |||
| const int node_count_before_invalid = conditionNodes(*before_invalid->condition); | |||
| LogicConditionPasteTarget invalid_target; | |||
| invalid_target.kind = LogicConditionPasteTargetKind::AfterNode; | |||
| invalid_target.expressionId = "missing-expression"; | |||
| service.clearHistory(); | |||
| require(!service.pasteConditionNodes( | |||
| logic_id, after_node_rung, {source}, invalid_target).succeeded | |||
| && conditionNodes(*service.findRung( | |||
| logic_id, after_node_rung)->condition) == node_count_before_invalid | |||
| && !service.canUndo(), | |||
| "an invalid paste target must roll back without recording history"); | |||
| } | |||
| } // namespace | |||
| int main() | |||
| @@ -1134,6 +1255,7 @@ int main() | |||
| testEdgeNodesAndRungComments(); | |||
| testHistoryAndAtomicBatchDelete(); | |||
| testBatchPasteNodesAndRung(); | |||
| testConditionPasteTargetsAndValidation(); | |||
| } | |||
| catch (const std::exception &error) | |||
| { | |||
| @@ -342,15 +342,17 @@ void testPlcPollQuantityBoundaries() | |||
| } | |||
| too_many_addresses.push_back({RegisterArea::D, 0}); | |||
| require(!service.setPollAddresses(too_many_addresses).succeeded, | |||
| "more than 1024 poll addresses must be rejected"); | |||
| "more than the configured poll address limit must be rejected"); | |||
| std::vector<RegisterAddress> too_many_blocks; | |||
| for (int index = 0; index < 65; ++index) | |||
| for (std::size_t index = 0; | |||
| index < ProjectLimits::kMaximumPollBlocks + 1U; | |||
| ++index) | |||
| { | |||
| too_many_blocks.push_back({RegisterArea::M, index * 2}); | |||
| too_many_blocks.push_back({RegisterArea::M, static_cast<int>(index * 2U)}); | |||
| } | |||
| require(!service.setPollAddresses(too_many_blocks).succeeded, | |||
| "more than 64 Modbus read blocks must be rejected"); | |||
| "more than the configured Modbus read block limit must be rejected"); | |||
| std::vector<RegisterAddress> maximum_contiguous_block; | |||
| for (int index = 0; index < ProjectLimits::kMaximumModbusReadCount; ++index) | |||
| @@ -719,7 +719,7 @@ void testQuantityFileLimits() | |||
| storage.load(too_many_pages_path.toStdString()); | |||
| require(!too_many_pages_result.succeeded | |||
| && too_many_pages_result.error == ProjectStorageError::InvalidField, | |||
| "a JSON page array over 128 items must be rejected"); | |||
| "a JSON page array over the configured limit must be rejected"); | |||
| service.editProject() = makeExampleProject(); | |||
| require(service.saveAs(valid_path.toStdString()).succeeded, | |||
| @@ -766,7 +766,7 @@ void testQuantityFileLimits() | |||
| const ProjectLoadResult deep_result = storage.load(deep_path.toStdString()); | |||
| require(!deep_result.succeeded | |||
| && deep_result.error == ProjectStorageError::InvalidField, | |||
| "a condition expression deeper than 20 levels must be rejected while parsing"); | |||
| "a condition expression deeper than the configured limit must be rejected while parsing"); | |||
| } | |||
| void testHmiPageJsonBoundaries() | |||