Pārlūkot izejas kodu

feat: 扩展多字数值类型与梯形图编辑能力

main
suyu pirms 3 nedēļām
vecāks
revīzija
d730d3bc1b
39 mainītis faili ar 216 papildinājumiem un 697 dzēšanām
  1. +0
    -5
      app/src/domain/control_logic_model.cpp
  2. +0
    -1
      app/src/domain/control_logic_model.h
  3. +0
    -10
      app/src/domain/hmi_model.cpp
  4. +0
    -2
      app/src/domain/hmi_model.h
  5. +0
    -7
      app/src/domain/project_limits.h
  6. +0
    -10
      app/src/domain/project_model.cpp
  7. +0
    -2
      app/src/domain/project_model.h
  8. +0
    -22
      app/src/domain/register_repository.h
  9. +1
    -2
      app/src/infrastructure/json_project_storage.h
  10. +0
    -10
      app/src/infrastructure/plc_communication_service.cpp
  11. +0
    -4
      app/src/infrastructure/plc_communication_service.h
  12. +0
    -7
      app/src/infrastructure/plc_register_repository.cpp
  13. +0
    -2
      app/src/infrastructure/plc_register_repository.h
  14. +1
    -1
      app/src/services/hmi_editor_service.h
  15. +0
    -309
      app/src/services/logic_editor_service.cpp
  16. +0
    -26
      app/src/services/logic_editor_service.h
  17. +0
    -5
      app/src/services/offline_simulation_service.cpp
  18. +0
    -2
      app/src/services/offline_simulation_service.h
  19. +1
    -5
      app/src/services/plc_communication_gateway.h
  20. +0
    -10
      app/src/services/project_service.cpp
  21. +1
    -3
      app/src/services/project_service.h
  22. +0
    -12
      app/src/services/register_monitor_service.cpp
  23. +0
    -4
      app/src/services/register_monitor_service.h
  24. +0
    -5
      app/src/ui/logic_editor_widget.cpp
  25. +0
    -1
      app/src/ui/logic_editor_widget.h
  26. +0
    -10
      app/src/ui/main_window.cpp
  27. +0
    -5
      app/src/ui/main_window.h
  28. +0
    -10
      app/src/ui/project_workspace_controller.cpp
  29. +0
    -5
      app/src/ui/project_workspace_controller.h
  30. +1
    -1
      app/tests/alarm_service_tests.cpp
  31. +67
    -65
      app/tests/domain_tests.cpp
  32. +14
    -14
      app/tests/hmi_editor_service_tests.cpp
  33. +38
    -64
      app/tests/logic_editor_service_tests.cpp
  34. +0
    -1
      app/tests/offline_simulation_service_tests.cpp
  35. +15
    -11
      app/tests/plc_runtime_tests.cpp
  36. +12
    -12
      app/tests/project_management_tests.cpp
  37. +18
    -10
      app/tests/register_monitor_service_tests.cpp
  38. +3
    -3
      app/tests/runtime_mode_service_tests.cpp
  39. +44
    -19
      app/tests/runtime_panel_controller_tests.cpp

+ 0
- 5
app/src/domain/control_logic_model.cpp Parādīt failu

@@ -531,11 +531,6 @@ bool LadderRung::validateForRunning(std::string *error) const
return true; return true;
} }


bool ControlLogic::validate(std::string *error) const
{
return validate(defaultProjectLimitSettings(), error);
}

bool ControlLogic::validate( bool ControlLogic::validate(
const ProjectLimitSettings &limits, std::string *error) const const ProjectLimitSettings &limits, std::string *error) const
{ {


+ 0
- 1
app/src/domain/control_logic_model.h Parādīt failu

@@ -175,7 +175,6 @@ struct ControlLogic
bool enabled = true; bool enabled = true;
std::vector<VerticalConnection> verticalConnections; std::vector<VerticalConnection> verticalConnections;


bool validate(std::string *error = nullptr) const;
bool validate( bool validate(
const ProjectLimitSettings &limits, const ProjectLimitSettings &limits,
std::string *error = nullptr) const; std::string *error = nullptr) const;


+ 0
- 10
app/src/domain/hmi_model.cpp Parādīt failu

@@ -247,11 +247,6 @@ bool HmiControl::isConfigured() const
return binding_area.has_value() && binding->area() == *binding_area; return binding_area.has_value() && binding->area() == *binding_area;
} }


bool HmiPage::validate(std::string *error) const
{
return validate(defaultProjectLimitSettings(), error);
}

bool HmiPage::validate( bool HmiPage::validate(
const ProjectLimitSettings &limits, std::string *error) const const ProjectLimitSettings &limits, std::string *error) const
{ {
@@ -316,11 +311,6 @@ bool HmiPage::validate(
return true; return true;
} }


bool HmiPage::validateForRunning(std::string *error) const
{
return validateForRunning(defaultProjectLimitSettings(), error);
}

bool HmiPage::validateForRunning( bool HmiPage::validateForRunning(
const ProjectLimitSettings &limits, std::string *error) const const ProjectLimitSettings &limits, std::string *error) const
{ {


+ 0
- 2
app/src/domain/hmi_model.h Parādīt failu

@@ -121,11 +121,9 @@ struct HmiPage
* @param error 可选错误输出,失败时写入首个校验原因 * @param error 可选错误输出,失败时写入首个校验原因
* @return 页面及其全部控件有效时返回 true * @return 页面及其全部控件有效时返回 true
*/ */
bool validate(std::string *error = nullptr) const;
bool validate( bool validate(
const ProjectLimitSettings &limits, const ProjectLimitSettings &limits,
std::string *error = nullptr) const; std::string *error = nullptr) const;
bool validateForRunning(std::string *error = nullptr) const;
bool validateForRunning( bool validateForRunning(
const ProjectLimitSettings &limits, const ProjectLimitSettings &limits,
std::string *error = nullptr) const; std::string *error = nullptr) const;


+ 0
- 7
app/src/domain/project_limits.h Parādīt failu

@@ -92,10 +92,3 @@ inline const ProjectLimitSettings &defaultProjectLimitSettings()
static const ProjectLimitSettings settings; static const ProjectLimitSettings settings;
return settings; return settings;
} }

// 返回长期有效的默认页面尺寸,供没有传入 INI 配置的代码使用
inline const HmiDefaultSettings &defaultHmiSettings()
{
static const HmiDefaultSettings settings;
return settings;
}

+ 0
- 10
app/src/domain/project_model.cpp Parādīt failu

@@ -156,11 +156,6 @@ const RegisterComment *Project::findRegisterComment(
return comment == registerComments.cend() ? nullptr : &*comment; return comment == registerComments.cend() ? nullptr : &*comment;
} }


bool Project::validate(std::string *error) const
{
return validate(defaultProjectLimitSettings(), error);
}

bool Project::validate( bool Project::validate(
const ProjectLimitSettings &limits, std::string *error) const const ProjectLimitSettings &limits, std::string *error) const
{ {
@@ -456,11 +451,6 @@ bool Project::validate(
return true; return true;
} }


bool Project::validateForRunning(std::string *error) const
{
return validateForRunning(defaultProjectLimitSettings(), error);
}

bool Project::validateForRunning( bool Project::validateForRunning(
const ProjectLimitSettings &limits, std::string *error) const const ProjectLimitSettings &limits, std::string *error) const
{ {


+ 0
- 2
app/src/domain/project_model.h Parādīt failu

@@ -48,12 +48,10 @@ struct Project
// 按地址查找工程注释,找不到时返回 nullptr // 按地址查找工程注释,找不到时返回 nullptr
const RegisterComment *findRegisterComment(const RegisterAddress &address) const; const RegisterComment *findRegisterComment(const RegisterAddress &address) const;
// 编辑态校验:允许尚未绑定完成的草稿 // 编辑态校验:允许尚未绑定完成的草稿
bool validate(std::string *error = nullptr) const;
bool validate( bool validate(
const ProjectLimitSettings &limits, const ProjectLimitSettings &limits,
std::string *error = nullptr) const; std::string *error = nullptr) const;
// 运行态校验:额外要求所有运行所需对象都已配置 // 运行态校验:额外要求所有运行所需对象都已配置
bool validateForRunning(std::string *error = nullptr) const;
bool validateForRunning( bool validateForRunning(
const ProjectLimitSettings &limits, const ProjectLimitSettings &limits,
std::string *error = nullptr) const; std::string *error = nullptr) const;


+ 0
- 22
app/src/domain/register_repository.h Parādīt failu

@@ -3,7 +3,6 @@
#include "register_address.h" #include "register_address.h"


#include <cstdint> #include <cstdint>
#include <array>
#include <utility> #include <utility>
#include <vector> #include <vector>


@@ -40,13 +39,6 @@ struct RegisterWriteResult
RegisterError error = RegisterError::Unavailable; // 写入失败原因 RegisterError error = RegisterError::Unavailable; // 写入失败原因
}; };


struct WordPairReadResult
{
bool succeeded = false;
std::array<std::int16_t, 2> values{};
RegisterError error = RegisterError::Unavailable;
};

struct WordsReadResult struct WordsReadResult
{ {
bool succeeded = false; bool succeeded = false;
@@ -118,18 +110,4 @@ public:
} }
return {true, RegisterError::None}; return {true, RegisterError::None};
} }
virtual WordPairReadResult readWordPair(const RegisterAddress &address) const
{
const WordsReadResult result = readWords(address, 2);
return result.succeeded
? WordPairReadResult{
true, {result.values[0], result.values[1]}, RegisterError::None}
: WordPairReadResult{false, {}, result.error};
}
virtual RegisterWriteResult writeWordPair(
const RegisterAddress &address,
const std::array<std::int16_t, 2> &values)
{
return writeWords(address, {values[0], values[1]});
}
}; };

+ 1
- 2
app/src/infrastructure/json_project_storage.h Parādīt failu

@@ -7,8 +7,7 @@ class JsonProjectStorage final : public ProjectStorage
{ {
public: public:
explicit JsonProjectStorage( explicit JsonProjectStorage(
const ProjectLimitSettings &project_limits =
defaultProjectLimitSettings());
const ProjectLimitSettings &project_limits);
ProjectSaveResult save( ProjectSaveResult save(
const Project &project, const std::string &file_path) override; const Project &project, const std::string &file_path) override;
ProjectLoadResult load(const std::string &file_path) override; ProjectLoadResult load(const std::string &file_path) override;


+ 0
- 10
app/src/infrastructure/plc_communication_service.cpp Parādīt failu

@@ -401,16 +401,6 @@ const std::string &PlcCommunicationService::lastError() const
return last_error_; return last_error_;
} }


const PlcSerialConfiguration &PlcCommunicationService::configuration() const
{
return configuration_;
}

const std::vector<PlcPollBlock> &PlcCommunicationService::pollBlocks() const
{
return poll_blocks_;
}

void PlcCommunicationService::setCallbacks( void PlcCommunicationService::setCallbacks(
std::function<void()> state_changed, std::function<void()> state_changed,
std::function<void(bool)> initial_read_changed, std::function<void(bool)> initial_read_changed,


+ 0
- 4
app/src/infrastructure/plc_communication_service.h Parādīt failu

@@ -58,10 +58,6 @@ public:
PlcCommunicationError lastErrorType() const override; PlcCommunicationError lastErrorType() const override;
// 返回最近一次通信错误的文字 // 返回最近一次通信错误的文字
const std::string &lastError() const override; const std::string &lastError() const override;
// 返回当前使用的串口和 Modbus 参数
const PlcSerialConfiguration &configuration() const;
// 返回当前拆分后的只读轮询计划,供诊断和边界测试使用
const std::vector<PlcPollBlock> &pollBlocks() const;
// 注册状态、首读、缓存更新和错误通知回调 // 注册状态、首读、缓存更新和错误通知回调
void setCallbacks( void setCallbacks(
std::function<void()> state_changed, std::function<void()> state_changed,


+ 0
- 7
app/src/infrastructure/plc_register_repository.cpp Parādīt failu

@@ -136,10 +136,3 @@ void PlcRegisterRepository::invalidate()
valid_bits_.fill(false); valid_bits_.fill(false);
valid_words_.fill(false); valid_words_.fill(false);
} }

bool PlcRegisterRepository::hasAnyValidValue() const
{
return std::any_of(valid_bits_.cbegin(), valid_bits_.cend(), [](bool value) { return value; })
|| std::any_of(
valid_words_.cbegin(), valid_words_.cend(), [](bool value) { return value; });
}

+ 0
- 2
app/src/infrastructure/plc_register_repository.h Parādīt failu

@@ -35,8 +35,6 @@ public:
void updateWord(int address, std::int16_t value); void updateWord(int address, std::int16_t value);
// 通信会话失效时清除“已读”标记,但保留数组内的旧值用于诊断 // 通信会话失效时清除“已读”标记,但保留数组内的旧值用于诊断
void invalidate(); void invalidate();
// 判断是否至少收到过一个有效 M/D 值
bool hasAnyValidValue() const;


private: private:
// M/D 区共用的数组长度,覆盖 0 到最大地址 // M/D 区共用的数组长度,覆盖 0 到最大地址


+ 1
- 1
app/src/services/hmi_editor_service.h Parādīt failu

@@ -61,7 +61,7 @@ public:
*/ */
explicit HmiEditorService( explicit HmiEditorService(
ProjectService &project_service, ProjectService &project_service,
const HmiDefaultSettings &hmi_defaults = defaultHmiSettings());
const HmiDefaultSettings &hmi_defaults);


/** /**
* @brief 按页面标识查找只读页面 * @brief 按页面标识查找只读页面


+ 0
- 309
app/src/services/logic_editor_service.cpp Parādīt failu

@@ -1245,46 +1245,6 @@ LogicEditorResult LogicEditorService::updateRungComment(
return {true, LogicEditorError::None, {}, rung_id}; return {true, LogicEditorError::None, {}, rung_id};
} }


LogicEditorResult LogicEditorService::setConditionAtColumn(
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured)
{
const LadderCell *cell = findCell(logic_id, rung_id, column);
if (cell == nullptr)
{
return failure(LogicEditorError::CellNotFound, "未找到目标条件网格");
}
if (!isConditionConfig(config))
{
return failure(LogicEditorError::InvalidNode, "条件区只能放置条件指令");
}
LogicNode candidate{"candidate", config, configured};
std::string error;
if (!candidate.validate(&error))
{
return failure(LogicEditorError::InvalidNode, error);
}
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
ControlLogic *logic = editableLogic(&project, logic_id);
LadderRung *rung = editableRung(logic, rung_id);
LadderCell &editable_cell = rung->cells[static_cast<std::size_t>(column)];
const std::string node_id = makeUniqueId(*logic, nodePrefix(config));
editable_cell.kind = LadderCellKind::Node;
editable_cell.node = LogicNode{node_id, config, configured};
if (!logic->validateStructure(project_service_.projectLimits(), &error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditResult LogicEditorService::applyConditionAndAdvance( LogicEditResult LogicEditorService::applyConditionAndAdvance(
const std::string &logic_id, const std::string &logic_id,
const LogicEditCursor &cursor, const LogicEditCursor &cursor,
@@ -1295,211 +1255,6 @@ LogicEditResult LogicEditorService::applyConditionAndAdvance(
logic_id, cursor, &config, configured); logic_id, cursor, &config, configured);
} }


LogicEditorResult LogicEditorService::insertConditionAtColumn(
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured)
{
const LadderRung *existing = findRung(logic_id, rung_id);
if (existing == nullptr || column < 0
|| column >= ProjectLimits::kMaximumConditionColumns)
{
return failure(LogicEditorError::CellNotFound, "未找到目标条件网格");
}
if (!isConditionConfig(config))
{
return failure(LogicEditorError::InvalidNode, "条件区只能放置条件指令");
}
if (existing->cells.back().kind != LadderCellKind::Gap)
{
return failure(
LogicEditorError::InvalidOperation,
"第 10 列已有内容,无法继续向右插入");
}

HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
ControlLogic *logic = editableLogic(&project, logic_id);
LadderRung *rung = editableRung(logic, rung_id);
for (int index = ProjectLimits::kMaximumConditionColumns - 1;
index > column;
--index)
{
rung->cells[static_cast<std::size_t>(index)] =
std::move(rung->cells[static_cast<std::size_t>(index - 1)]);
}
// 插入列会让右侧网格右移,边界 10 是固定输出侧边界,不能继续右移
for (VerticalConnection &connection : logic->verticalConnections)
{
if ((connection.upperRungId == rung_id
|| connection.lowerRungId == rung_id)
&& connection.columnBoundary >= column
&& connection.columnBoundary < ProjectLimits::kMaximumConditionColumns)
{
++connection.columnBoundary;
}
}
LadderCell inserted;
inserted.id = makeUniqueId(*logic, "cell");
inserted.kind = LadderCellKind::Node;
const std::string node_id = makeUniqueId(*logic, nodePrefix(config));
inserted.node = LogicNode{node_id, config, configured};
rung->cells[static_cast<std::size_t>(column)] = std::move(inserted);
std::string error;
if (!logic->validateStructure(project_service_.projectLimits(), &error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::appendCondition(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config,
bool configured)
{
const ControlLogic *logic = findLogic(logic_id);
if (logic == nullptr)
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
if (!isConditionConfig(config))
{
return failure(LogicEditorError::InvalidNode, "条件区只能放置条件指令");
}
std::string target_rung_id = rung_id;
if (target_rung_id.empty())
{
if (logic->rungs.size()
>= project_service_.projectLimits().maximumRungsPerLogic
|| totalRungCount(project_service_.project())
>= ProjectLimits::kMaximumRungsPerProject)
{
return failure(
LogicEditorError::InvalidOperation,
"梯形图行数已经达到当前上限");
}
// 新建首行和放置首个节点必须共用一条历史记录
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
ControlLogic *editable = editableLogic(&project, logic_id);
target_rung_id = insertEmptyRungAt(editable, editable->rungs.size());
LadderRung *created = editableRung(editable, target_rung_id);
const auto empty = std::find_if(
created->cells.begin(), created->cells.end(),
[](const LadderCell &cell)
{
return cell.kind == LadderCellKind::Gap;
});
if (empty == created->cells.end())
{
rollbackEdit(std::move(before), modified_before);
return failure(
LogicEditorError::InvalidOperation,
"条件区 10 列已经占满");
}
const std::string node_id = makeUniqueId(*editable, nodePrefix(config));
LadderCell &cell = created->cells[
static_cast<std::size_t>(std::distance(created->cells.begin(), empty))];
cell.kind = LadderCellKind::Node;
cell.node = LogicNode{node_id, config, configured};
std::string error;
if (!editable->validateStructure(project_service_.projectLimits(), &error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}
const LadderRung *rung = findRung(logic_id, target_rung_id);
if (rung == nullptr)
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图行");
}
const auto empty = std::find_if(
rung->cells.cbegin(), rung->cells.cend(),
[](const LadderCell &cell) { return cell.kind == LadderCellKind::Gap; });
if (empty == rung->cells.cend())
{
return failure(LogicEditorError::InvalidOperation, "条件区 10 列已经占满");
}
return setConditionAtColumn(
logic_id,
target_rung_id,
static_cast<int>(std::distance(rung->cells.cbegin(), empty)),
config,
configured);
}

LogicEditorResult LogicEditorService::appendWire(
const std::string &logic_id,
const std::string &rung_id,
int column_span)
{
const ControlLogic *logic = findLogic(logic_id);
if (logic == nullptr)
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
if (column_span <= 0
|| column_span > ProjectLimits::kMaximumConditionColumns)
{
return failure(LogicEditorError::InvalidOperation, "横线参数无效");
}
if (rung_id.empty())
{
if (logic->rungs.size()
>= project_service_.projectLimits().maximumRungsPerLogic
|| totalRungCount(project_service_.project())
>= ProjectLimits::kMaximumRungsPerProject)
{
return failure(
LogicEditorError::InvalidOperation,
"梯形图行数已经达到当前上限");
}
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
ControlLogic *editable = editableLogic(&project, logic_id);
const std::string new_rung_id = insertEmptyRungAt(
editable, editable->rungs.size());
LadderRung *rung = editableRung(editable, new_rung_id);
const int first = ProjectLimits::kMaximumConditionColumns - column_span;
for (int column = first;
column < ProjectLimits::kMaximumConditionColumns;
++column)
{
rung->cells[static_cast<std::size_t>(column)].kind =
LadderCellKind::Wire;
}
std::string error;
if (!editable->validateStructure(project_service_.projectLimits(), &error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, new_rung_id};
}
const LadderRung *rung = findRung(logic_id, rung_id);
if (rung == nullptr)
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图行");
}
const int first = ProjectLimits::kMaximumConditionColumns - column_span;
return setHorizontalWireRange(logic_id, rung_id, first,
ProjectLimits::kMaximumConditionColumns - 1,
true);
}

LogicEditResult LogicEditorService::applyWireAndAdvance( LogicEditResult LogicEditorService::applyWireAndAdvance(
const std::string &logic_id, const std::string &logic_id,
const LogicEditCursor &cursor) const LogicEditCursor &cursor)
@@ -3055,70 +2810,6 @@ bool LogicEditorService::areConditionNodesContiguous(
== static_cast<int>(columns.size()); == static_cast<int>(columns.size());
} }


LogicEditorResult LogicEditorService::pasteRung(
const std::string &logic_id,
const LadderRung &source)
{
const ControlLogic *logic = findLogic(logic_id);
std::string error;
if (logic == nullptr)
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
if (!source.validateStructure(&error))
{
return failure(LogicEditorError::InvalidOperation, error);
}
if (logic->rungs.size()
>= project_service_.projectLimits().maximumRungsPerLogic
|| totalRungCount(project_service_.project())
>= ProjectLimits::kMaximumRungsPerProject)
{
return failure(LogicEditorError::InvalidOperation, "梯形图行数已经达到上限");
}
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
ControlLogic *editable = editableLogic(&project, logic_id);
LadderRung pasted = makeEmptyRung(*editable, editable->rungs.size());
pasted.comment = source.comment;
for (std::size_t column = 0U; column < source.cells.size(); ++column)
{
pasted.cells[column].kind = source.cells[column].kind;
if (source.cells[column].node.has_value())
{
const LogicNode &source_node = *source.cells[column].node;
pasted.cells[column].node = LogicNode{
makeUniqueId(*editable, nodePrefix(source_node.config)),
source_node.config,
source_node.configured};
}
}
if (source.output.has_value())
{
pasted.output = LogicNode{
makeUniqueId(*editable, nodePrefix(source.output->config)),
source.output->config,
source.output->configured};
}
const std::string pasted_id = pasted.id;
editable->rungs.push_back(std::move(pasted));
refreshRungNames(editable);
if (!editable->validateStructure(project_service_.projectLimits(), &error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, pasted_id};
}

LogicEditorResult LogicEditorService::removeNode(
const std::string &logic_id, const std::string &node_id)
{
return removeNodes(logic_id, {node_id});
}

LogicEditorResult LogicEditorService::removeNodes( LogicEditorResult LogicEditorService::removeNodes(
const std::string &logic_id, const std::string &logic_id,
const std::vector<std::string> &node_ids) const std::vector<std::string> &node_ids)


+ 0
- 26
app/src/services/logic_editor_service.h Parādīt failu

@@ -214,38 +214,17 @@ public:
const std::string &rung_id, const std::string &rung_id,
const std::string &comment); const std::string &comment);


LogicEditorResult setConditionAtColumn(
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured = false);
LogicEditResult applyConditionAndAdvance( LogicEditResult applyConditionAndAdvance(
const std::string &logic_id, const std::string &logic_id,
const LogicEditCursor &cursor, const LogicEditCursor &cursor,
const LogicNodeConfig &config, const LogicNodeConfig &config,
bool configured = false); bool configured = false);
LogicEditorResult insertConditionAtColumn(
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured = false);
LogicEditorResult appendCondition(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config,
bool configured = false);
LogicEditorResult setHorizontalWireRange( LogicEditorResult setHorizontalWireRange(
const std::string &logic_id, const std::string &logic_id,
const std::string &rung_id, const std::string &rung_id,
int first_column, int first_column,
int last_column, int last_column,
bool connected); bool connected);
LogicEditorResult appendWire(
const std::string &logic_id,
const std::string &rung_id,
int column_span = 1);
LogicEditResult applyWireAndAdvance( LogicEditResult applyWireAndAdvance(
const std::string &logic_id, const std::string &logic_id,
const LogicEditCursor &cursor); const LogicEditCursor &cursor);
@@ -320,11 +299,6 @@ public:
const std::string &logic_id, const std::string &logic_id,
const std::string &rung_id, const std::string &rung_id,
const std::vector<std::string> &node_ids) const; const std::vector<std::string> &node_ids) const;
LogicEditorResult pasteRung(
const std::string &logic_id,
const LadderRung &source);
LogicEditorResult removeNode(
const std::string &logic_id, const std::string &node_id);
LogicEditorResult removeNodes( LogicEditorResult removeNodes(
const std::string &logic_id, const std::string &logic_id,
const std::vector<std::string> &node_ids); const std::vector<std::string> &node_ids);


+ 0
- 5
app/src/services/offline_simulation_service.cpp Parādīt failu

@@ -105,11 +105,6 @@ SimulationState OfflineSimulationService::state() const
return state_; return state_;
} }


int OfflineSimulationService::scanIntervalMs() const
{
return timer_.interval();
}

std::uint64_t OfflineSimulationService::successfulScanCount() const std::uint64_t OfflineSimulationService::successfulScanCount() const
{ {
return successful_scan_count_; return successful_scan_count_;


+ 0
- 2
app/src/services/offline_simulation_service.h Parādīt failu

@@ -64,8 +64,6 @@ public:


// 返回当前离线仿真生命周期状态 // 返回当前离线仿真生命周期状态
SimulationState state() const; SimulationState state() const;
// 返回当前定时器扫描周期,单位为毫秒
int scanIntervalMs() const;
// 返回本次会话成功完成的扫描轮数 // 返回本次会话成功完成的扫描轮数
std::uint64_t successfulScanCount() const; std::uint64_t successfulScanCount() const;
// 返回最近一次扫描错误;无错误时为成功结果 // 返回最近一次扫描错误;无错误时为成功结果


+ 1
- 5
app/src/services/plc_communication_gateway.h Parādīt failu

@@ -163,11 +163,7 @@ public:
*/ */
virtual PlcCommunicationResult setPollAddresses( virtual PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses, const std::vector<RegisterAddress> &addresses,
const std::vector<RegisterWordRange> &multi_word_ranges)
{
(void)multi_word_ranges;
return setPollAddresses(addresses);
}
const std::vector<RegisterWordRange> &multi_word_ranges) = 0;


// 返回当前连接生命周期状态;Connected 不代表首读资格已经完成 // 返回当前连接生命周期状态;Connected 不代表首读资格已经完成
virtual PlcConnectionState state() const = 0; virtual PlcConnectionState state() const = 0;


+ 0
- 10
app/src/services/project_service.cpp Parādīt failu

@@ -24,11 +24,6 @@ std::string generateProjectId()


} // namespace } // namespace


ProjectService::ProjectService(ProjectStorage &storage)
: ProjectService(storage, defaultProjectLimitSettings())
{
}

ProjectService::ProjectService( ProjectService::ProjectService(
ProjectStorage &storage, ProjectStorage &storage,
const ProjectLimitSettings &project_limits) const ProjectLimitSettings &project_limits)
@@ -49,11 +44,6 @@ Project &ProjectService::editProject()
return project_; return project_;
} }


const std::string &ProjectService::currentFilePath() const
{
return current_file_path_;
}

bool ProjectService::hasCurrentFile() const bool ProjectService::hasCurrentFile() const
{ {
return !current_file_path_.empty(); return !current_file_path_.empty();


+ 1
- 3
app/src/services/project_service.h Parādīt failu

@@ -27,8 +27,7 @@ struct ProjectOperationResult
class ProjectService class ProjectService
{ {
public: public:
// 使用外部提供的工程存储实现创建服务
explicit ProjectService(ProjectStorage &storage);
// 使用外部提供的工程存储实现和启动配置创建服务
ProjectService( ProjectService(
ProjectStorage &storage, ProjectStorage &storage,
const ProjectLimitSettings &project_limits); const ProjectLimitSettings &project_limits);
@@ -37,7 +36,6 @@ public:
const Project &project() const; const Project &project() const;
// 获取服务层授权的可编辑引用,调用方必须随后完成校验 // 获取服务层授权的可编辑引用,调用方必须随后完成校验
Project &editProject(); Project &editProject();
const std::string &currentFilePath() const;
bool hasCurrentFile() const; bool hasCurrentFile() const;
bool isModified() const; bool isModified() const;
const ProjectLimitSettings &projectLimits() const; const ProjectLimitSettings &projectLimits() const;


+ 0
- 12
app/src/services/register_monitor_service.cpp Parādīt failu

@@ -225,18 +225,6 @@ RegisterMonitorWriteResult RegisterMonitorService::writeBit(
return captureInitialBit(address, value, active_result); return captureInitialBit(address, value, active_result);
} }


RegisterMonitorWriteResult RegisterMonitorService::writeWord(
const RegisterAddress &address, std::int16_t value)
{
return writeNumeric(address, RegisterDataType::Int16, value);
}

RegisterMonitorWriteResult RegisterMonitorService::writeFloat(
const RegisterAddress &address, float value)
{
return writeNumeric(address, RegisterDataType::Float32, value);
}

RegisterMonitorWriteResult RegisterMonitorService::writeNumeric( RegisterMonitorWriteResult RegisterMonitorService::writeNumeric(
const RegisterAddress &address, RegisterDataType data_type, double value) const RegisterAddress &address, RegisterDataType data_type, double value)
{ {


+ 0
- 4
app/src/services/register_monitor_service.h Parādīt failu

@@ -91,10 +91,6 @@ public:
* @param value 要写入的 D 字值 * @param value 要写入的 D 字值
* @return 成功或包含底层 RegisterError 和用户可读文本的失败结果 * @return 成功或包含底层 RegisterError 和用户可读文本的失败结果
*/ */
RegisterMonitorWriteResult writeWord(
const RegisterAddress &address, std::int16_t value);
RegisterMonitorWriteResult writeFloat(
const RegisterAddress &address, float value);
RegisterMonitorWriteResult writeNumeric( RegisterMonitorWriteResult writeNumeric(
const RegisterAddress &address, RegisterDataType data_type, double value); const RegisterAddress &address, RegisterDataType data_type, double value);
/** /**


+ 0
- 5
app/src/ui/logic_editor_widget.cpp Parādīt failu

@@ -840,11 +840,6 @@ void LogicEditorWidget::clearRuntimeTrace()
rebuildScene(); rebuildScene();
} }


bool LogicEditorWidget::runtimeTraceEnabled() const
{
return runtime_trace_enabled_;
}

void LogicEditorWidget::reloadLogic() void LogicEditorWidget::reloadLogic()
{ {
setLogicId(logic_id_); setLogicId(logic_id_);


+ 0
- 1
app/src/ui/logic_editor_widget.h Parādīt failu

@@ -40,7 +40,6 @@ public:
const LogicTraceSnapshot &trace, const LogicTraceSnapshot &trace,
const std::string &fault_node_id = {}); const std::string &fault_node_id = {});
void clearRuntimeTrace(); void clearRuntimeTrace();
bool runtimeTraceEnabled() const;
void reloadLogic(); void reloadLogic();
void clearSelection(); void clearSelection();
void selectNode(const std::string &node_id); void selectNode(const std::string &node_id);


+ 0
- 10
app/src/ui/main_window.cpp Parādīt failu

@@ -576,16 +576,6 @@ void MainWindow::closeEvent(QCloseEvent *event)
QMainWindow::closeEvent(event); QMainWindow::closeEvent(event);
} }


const std::string &MainWindow::currentHmiPageId() const
{
return current_hmi_page_id_;
}

const std::string &MainWindow::currentLogicId() const
{
return current_logic_id_;
}

void MainWindow::configureActions() void MainWindow::configureActions()
{ {
mode_action_group_ = new QActionGroup(this); mode_action_group_ = new QActionGroup(this);


+ 0
- 5
app/src/ui/main_window.h Parādīt failu

@@ -111,11 +111,6 @@ public:
/** 释放窗口拥有的控制器和可选导航服务 */ /** 释放窗口拥有的控制器和可选导航服务 */
~MainWindow() override; ~MainWindow() override;


/** 返回当前选中的 HMI 页面标识 */
const std::string &currentHmiPageId() const;
/** 返回当前选中的控制逻辑标识 */
const std::string &currentLogicId() const;

protected: protected:
/** 处理主窗口及子控件的事件过滤请求 */ /** 处理主窗口及子控件的事件过滤请求 */
bool eventFilter(QObject *watched, QEvent *event) override; bool eventFilter(QObject *watched, QEvent *event) override;


+ 0
- 10
app/src/ui/project_workspace_controller.cpp Parādīt failu

@@ -256,16 +256,6 @@ void ProjectWorkspaceController::refresh()
updateActions(); updateActions();
} }


const std::string &ProjectWorkspaceController::currentHmiPageId() const
{
return current_hmi_page_id_;
}

const std::string &ProjectWorkspaceController::currentLogicId() const
{
return current_logic_id_;
}

void ProjectWorkspaceController::handleSelectionChanged() void ProjectWorkspaceController::handleSelectionChanged()
{ {
QTreeWidgetItem *item = ui_.projectTree->currentItem(); QTreeWidgetItem *item = ui_.projectTree->currentItem();


+ 0
- 5
app/src/ui/project_workspace_controller.h Parādīt failu

@@ -89,11 +89,6 @@ public:
/** 启用或停用当前选中的控制逻辑 */ /** 启用或停用当前选中的控制逻辑 */
void toggleSelectedLogicEnabled(); void toggleSelectedLogicEnabled();


/** 返回当前 HMI 页面标识 */
const std::string &currentHmiPageId() const;
/** 返回当前控制逻辑标识 */
const std::string &currentLogicId() const;

private: private:
/** 工程树项目的三种业务类型 */ /** 工程树项目的三种业务类型 */
enum class ItemKind enum class ItemKind


+ 1
- 1
app/tests/alarm_service_tests.cpp Parādīt failu

@@ -33,7 +33,7 @@ const AlarmRecord *findRecord(
void testAlarmDefinitionsAndRuntimeLifecycle() void testAlarmDefinitionsAndRuntimeLifecycle()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
AlarmEditorService editor_service(project_service); AlarmEditorService editor_service(project_service);
VirtualRegisterRepository repository; VirtualRegisterRepository repository;
AlarmService alarm_service(project_service, repository); AlarmService alarm_service(project_service, repository);


+ 67
- 65
app/tests/domain_tests.cpp Parādīt failu

@@ -159,13 +159,15 @@ void testMultiWordCodecsAndBlockAccess()


VirtualRegisterRepository repository; VirtualRegisterRepository repository;
const RegisterAddress d10{RegisterArea::D, 10}; const RegisterAddress d10{RegisterArea::D, 10};
require(repository.writeWordPair(d10, Float32Codec::encode(-2.5f)).succeeded,
const auto float_words = Float32Codec::encode(-2.5f);
require(repository.writeWords(
d10, {float_words[0], float_words[1]}).succeeded,
"virtual repository must write Float32 low and high words"); "virtual repository must write Float32 low and high words");
const WordPairReadResult pair = repository.readWordPair(d10);
const WordsReadResult pair = repository.readWords(d10, 2);
require(pair.succeeded require(pair.succeeded
&& Float32Codec::decode(pair.values[0], pair.values[1]).value() == -2.5f, && Float32Codec::decode(pair.values[0], pair.values[1]).value() == -2.5f,
"virtual repository must read Float32 from two consecutive words"); "virtual repository must read Float32 from two consecutive words");
require(!repository.writeWordPair({RegisterArea::D, 4000}, {}).succeeded,
require(!repository.writeWords({RegisterArea::D, 4000}, {}).succeeded,
"Float32 write at D4000 must be rejected"); "Float32 write at D4000 must be rejected");
require(repository.writeWords( require(repository.writeWords(
{RegisterArea::D, 30}, {RegisterArea::D, 30},
@@ -294,12 +296,12 @@ void testMultiWordHmiBoundaries()
display.binding = RegisterAddress{RegisterArea::D, 3999}; display.binding = RegisterAddress{RegisterArea::D, 3999};
display.dataType = RegisterDataType::Float32; display.dataType = RegisterDataType::Float32;
project.hmiPages.front().controls.push_back(display); project.hmiPages.front().controls.push_back(display);
require(project.validate(),
require(project.validate(defaultProjectLimitSettings()),
"Float32 D3999 must be valid and occupy D3999~D4000"); "Float32 D3999 must be valid and occupy D3999~D4000");


project.hmiPages.front().controls.back().binding = project.hmiPages.front().controls.back().binding =
RegisterAddress{RegisterArea::D, 4000}; RegisterAddress{RegisterArea::D, 4000};
require(!project.validate(), "Float32 D4000 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "Float32 D4000 must be rejected");


project = makeValidProject(); project = makeValidProject();
HmiControl int32_display = display; HmiControl int32_display = display;
@@ -307,10 +309,10 @@ void testMultiWordHmiBoundaries()
int32_display.dataType = RegisterDataType::Int32; int32_display.dataType = RegisterDataType::Int32;
int32_display.binding = RegisterAddress{RegisterArea::D, 3999}; int32_display.binding = RegisterAddress{RegisterArea::D, 3999};
project.hmiPages.front().controls.push_back(int32_display); project.hmiPages.front().controls.push_back(int32_display);
require(project.validate(), "Int32 D3999 must be valid");
require(project.validate(defaultProjectLimitSettings()), "Int32 D3999 must be valid");
project.hmiPages.front().controls.back().binding = project.hmiPages.front().controls.back().binding =
RegisterAddress{RegisterArea::D, 4000}; RegisterAddress{RegisterArea::D, 4000};
require(!project.validate(), "Int32 D4000 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "Int32 D4000 must be rejected");


project = makeValidProject(); project = makeValidProject();
HmiControl double_display = display; HmiControl double_display = display;
@@ -318,13 +320,13 @@ void testMultiWordHmiBoundaries()
double_display.dataType = RegisterDataType::Float64; double_display.dataType = RegisterDataType::Float64;
double_display.binding = RegisterAddress{RegisterArea::D, 3996}; double_display.binding = RegisterAddress{RegisterArea::D, 3996};
project.hmiPages.front().controls.push_back(double_display); project.hmiPages.front().controls.push_back(double_display);
require(project.validate(), "Double D3996 must be a valid even start address");
require(project.validate(defaultProjectLimitSettings()), "Double D3996 must be a valid even start address");
project.hmiPages.front().controls.back().binding = project.hmiPages.front().controls.back().binding =
RegisterAddress{RegisterArea::D, 3997}; RegisterAddress{RegisterArea::D, 3997};
require(!project.validate(), "Double D3997 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "Double D3997 must be rejected");
project.hmiPages.front().controls.back().binding = project.hmiPages.front().controls.back().binding =
RegisterAddress{RegisterArea::D, 3995}; RegisterAddress{RegisterArea::D, 3995};
require(!project.validate(), "Double odd start addresses must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "Double odd start addresses must be rejected");


project = makeValidProject(); project = makeValidProject();
HmiControl invalid_display = display; HmiControl invalid_display = display;
@@ -332,7 +334,7 @@ void testMultiWordHmiBoundaries()
invalid_display.binding.reset(); invalid_display.binding.reset();
invalid_display.dataType = static_cast<RegisterDataType>(99); invalid_display.dataType = static_cast<RegisterDataType>(99);
project.hmiPages.front().controls.push_back(invalid_display); project.hmiPages.front().controls.push_back(invalid_display);
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"an unbound HMI draft must still reject an unknown numeric type"); "an unbound HMI draft must still reject an unknown numeric type");


project = makeValidProject(); project = makeValidProject();
@@ -342,12 +344,12 @@ void testMultiWordHmiBoundaries()
second.id = "float-display-duplicate"; second.id = "float-display-duplicate";
project.hmiPages.front().controls.push_back(first); project.hmiPages.front().controls.push_back(first);
project.hmiPages.front().controls.push_back(second); project.hmiPages.front().controls.push_back(second);
require(project.validate(),
require(project.validate(defaultProjectLimitSettings()),
"same Float32 start address and type may be bound more than once"); "same Float32 start address and type may be bound more than once");


second.dataType = RegisterDataType::Int16; second.dataType = RegisterDataType::Int16;
project.hmiPages.front().controls.back() = second; project.hmiPages.front().controls.back() = second;
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"different HMI data types may not partially overlap"); "different HMI data types may not partially overlap");


for (const RegisterDataType type : { for (const RegisterDataType type : {
@@ -372,7 +374,7 @@ void testMultiWordHmiBoundaries()
RegisterArea::D, RegisterArea::D,
10 + registerDataTypeWordCount(type) - 1}}, 10 + registerDataTypeWordCount(type) - 1}},
true}; true};
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"16-bit instructions must not write inside any multi-word HMI range"); "16-bit instructions must not write inside any multi-word HMI range");
} }
} }
@@ -386,28 +388,28 @@ void testHmiAppearancePropertyBoundaries()
button.properties[HmiAppearanceProperty::kFontSize] = "18"; button.properties[HmiAppearanceProperty::kFontSize] = "18";
button.properties[HmiAppearanceProperty::kFontBold] = "true"; button.properties[HmiAppearanceProperty::kFontBold] = "true";
button.properties[HmiAppearanceProperty::kFontItalic] = "false"; button.properties[HmiAppearanceProperty::kFontItalic] = "false";
require(project.validate(), "valid HMI appearance properties must pass validation");
require(project.validate(defaultProjectLimitSettings()), "valid HMI appearance properties must pass validation");


button.properties[HmiAppearanceProperty::kTextColor] = "red"; button.properties[HmiAppearanceProperty::kTextColor] = "red";
require(!project.validate(), "text colors must use the #RRGGBB format");
require(!project.validate(defaultProjectLimitSettings()), "text colors must use the #RRGGBB format");


project = makeValidProject(); project = makeValidProject();
HmiControl &font_control = project.hmiPages.front().controls.front(); HmiControl &font_control = project.hmiPages.front().controls.front();
font_control.properties[HmiAppearanceProperty::kFontSize] = "5"; font_control.properties[HmiAppearanceProperty::kFontSize] = "5";
require(!project.validate(), "font sizes below the minimum must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "font sizes below the minimum must be rejected");
font_control.properties[HmiAppearanceProperty::kFontSize] = "73"; font_control.properties[HmiAppearanceProperty::kFontSize] = "73";
require(!project.validate(), "font sizes above the maximum must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "font sizes above the maximum must be rejected");
font_control.properties[HmiAppearanceProperty::kFontSize] = "large"; font_control.properties[HmiAppearanceProperty::kFontSize] = "large";
require(!project.validate(), "non-numeric font sizes must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "non-numeric font sizes must be rejected");


project = makeValidProject(); project = makeValidProject();
HmiControl &style_control = project.hmiPages.front().controls.front(); HmiControl &style_control = project.hmiPages.front().controls.front();
style_control.properties[HmiAppearanceProperty::kFontBold] = "yes"; style_control.properties[HmiAppearanceProperty::kFontBold] = "yes";
require(!project.validate(), "font style flags must be true or false");
require(!project.validate(defaultProjectLimitSettings()), "font style flags must be true or false");


project = makeValidProject(); project = makeValidProject();
project.hmiPages.front().controls.front().properties["legacyColor"] = "green"; project.hmiPages.front().controls.front().properties["legacyColor"] = "green";
require(project.validate(), "unknown HMI extension properties must remain supported");
require(project.validate(defaultProjectLimitSettings()), "unknown HMI extension properties must remain supported");
} }


Project makeValidProject() Project makeValidProject()
@@ -475,11 +477,11 @@ void testMultiPageAndLogicDomainRules()
label.type = HmiControlType::Label; label.type = HmiControlType::Label;
label.text = "Machine"; label.text = "Machine";
project.hmiPages.front().controls.push_back(label); project.hmiPages.front().controls.push_back(label);
require(project.validate(), "an unbound label must be a valid static control");
require(project.validate(defaultProjectLimitSettings()), "an unbound label must be a valid static control");


project.hmiPages.front().controls.back().binding = project.hmiPages.front().controls.back().binding =
RegisterAddress{RegisterArea::M, 10}; RegisterAddress{RegisterArea::M, 10};
require(!project.validate(), "labels must reject register bindings");
require(!project.validate(defaultProjectLimitSettings()), "labels must reject register bindings");


project = makeValidProject(); project = makeValidProject();
project.hmiPages.push_back(settings); project.hmiPages.push_back(settings);
@@ -489,21 +491,21 @@ void testMultiPageAndLogicDomainRules()
jump.text = "Settings"; jump.text = "Settings";
jump.pageJump = HmiPageJumpConfig{settings.id}; jump.pageJump = HmiPageJumpConfig{settings.id};
project.hmiPages.front().controls.push_back(jump); project.hmiPages.front().controls.push_back(jump);
require(project.validate() && project.validateForRunning(),
require(project.validate(defaultProjectLimitSettings()) && project.validateForRunning(defaultProjectLimitSettings()),
"a page jump must resolve its target by stable page id"); "a page jump must resolve its target by stable page id");


project.hmiPages.front().controls.back().pageJump->targetPageId = "missing"; project.hmiPages.front().controls.back().pageJump->targetPageId = "missing";
require(!project.validate(), "a page jump must reject a missing target page");
require(!project.validate(defaultProjectLimitSettings()), "a page jump must reject a missing target page");


project = makeValidProject(); project = makeValidProject();
project.initialHmiPageId = "missing"; project.initialHmiPageId = "missing";
require(!project.validate(), "the initial HMI page id must resolve to a page");
require(!project.validate(defaultProjectLimitSettings()), "the initial HMI page id must resolve to a page");


project = makeValidProject(); project = makeValidProject();
HmiPage duplicate_name = settings; HmiPage duplicate_name = settings;
duplicate_name.name = project.hmiPages.front().name; duplicate_name.name = project.hmiPages.front().name;
project.hmiPages.push_back(duplicate_name); project.hmiPages.push_back(duplicate_name);
require(!project.validate(), "HMI page names must be unique");
require(!project.validate(defaultProjectLimitSettings()), "HMI page names must be unique");


project = makeValidProject(); project = makeValidProject();
ControlLogic disabled_draft; ControlLogic disabled_draft;
@@ -515,17 +517,17 @@ void testMultiPageAndLogicDomainRules()
draft_rung.name = "Draft network"; draft_rung.name = "Draft network";
disabled_draft.rungs.push_back(std::move(draft_rung)); disabled_draft.rungs.push_back(std::move(draft_rung));
project.controlLogics.push_back(disabled_draft); project.controlLogics.push_back(disabled_draft);
require(project.validateForRunning(),
require(project.validateForRunning(defaultProjectLimitSettings()),
"a disabled draft logic must not block offline running"); "a disabled draft logic must not block offline running");


project.controlLogics.back().name = project.controlLogics.front().name; project.controlLogics.back().name = project.controlLogics.front().name;
require(!project.validate(), "control logic names must be unique");
require(!project.validate(defaultProjectLimitSettings()), "control logic names must be unique");


project = makeValidProject(); project = makeValidProject();
project.hmiPages.front().controls.front().type = project.hmiPages.front().controls.front().type =
static_cast<HmiControlType>(99); static_cast<HmiControlType>(99);
project.hmiPages.front().controls.front().binding.reset(); project.hmiPages.front().controls.front().binding.reset();
require(!project.validate(), "unknown HMI control types must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "unknown HMI control types must be rejected");
} }


void testQuantityBoundaries() void testQuantityBoundaries()
@@ -540,9 +542,9 @@ void testQuantityBoundaries()
400, 400,
{}}); {}});
} }
require(project.validate(), "an HMI page count at the configured limit must be accepted");
require(project.validate(defaultProjectLimitSettings()), "an HMI page count at the configured limit must be accepted");
project.hmiPages.push_back({"page-over", "Page over", 800, 400, {}}); project.hmiPages.push_back({"page-over", "Page over", 800, 400, {}});
require(!project.validate(), "an HMI page count of 129 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "an HMI page count of 129 must be rejected");


project = makeValidProject(); project = makeValidProject();
project.hmiPages.clear(); project.hmiPages.clear();
@@ -569,7 +571,7 @@ void testQuantityBoundaries()
project.hmiPages.push_back(std::move(page)); project.hmiPages.push_back(std::move(page));
} }
project.initialHmiPageId = project.hmiPages.front().id; project.initialHmiPageId = project.hmiPages.front().id;
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"an HMI control count over the project limit must be rejected"); "an HMI control count over the project limit must be rejected");


project = makeValidProject(); project = makeValidProject();
@@ -585,14 +587,14 @@ void testQuantityBoundaries()
label.text = "label"; label.text = "label";
project.hmiPages.front().controls.push_back(std::move(label)); project.hmiPages.front().controls.push_back(std::move(label));
} }
require(project.validate(), "a page control count at the configured limit must be accepted");
require(project.validate(defaultProjectLimitSettings()), "a page control count at the configured limit must be accepted");
HmiControl extra_label; HmiControl extra_label;
extra_label.id = "label-over"; extra_label.id = "label-over";
extra_label.type = HmiControlType::Label; extra_label.type = HmiControlType::Label;
extra_label.bounds = {0, 0, 1, 1}; extra_label.bounds = {0, 0, 1, 1};
extra_label.text = "label"; extra_label.text = "label";
project.hmiPages.front().controls.push_back(std::move(extra_label)); project.hmiPages.front().controls.push_back(std::move(extra_label));
require(!project.validate(), "a page control count of 513 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "a page control count of 513 must be rejected");


project = makeValidProject(); project = makeValidProject();
project.controlLogics.clear(); project.controlLogics.clear();
@@ -616,24 +618,24 @@ void testQuantityBoundaries()
} }
project.controlLogics.push_back(std::move(logic)); project.controlLogics.push_back(std::move(logic));
} }
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"a ladder rung count over the project limit must be rejected"); "a ladder rung count over the project limit must be rejected");


project = makeValidProject(); project = makeValidProject();
project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth; project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth;
project.hmiPages.front().height = ProjectLimits::kMaximumHmiPageHeight; project.hmiPages.front().height = ProjectLimits::kMaximumHmiPageHeight;
require(project.validate(), "an HMI page size of 1600 by 800 must be accepted");
require(project.validate(defaultProjectLimitSettings()), "an HMI page size of 1600 by 800 must be accepted");
project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth + 1; project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth + 1;
require(!project.validate(), "an HMI page width of 1601 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "an HMI page width of 1601 must be rejected");
project.hmiPages.front().width = ProjectLimits::kMinimumHmiPageWidth - 1; project.hmiPages.front().width = ProjectLimits::kMinimumHmiPageWidth - 1;
require(!project.validate(), "an HMI page width of 319 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "an HMI page width of 319 must be rejected");
project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth; project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth;
project.hmiPages.front().height = ProjectLimits::kMinimumHmiPageHeight - 1; project.hmiPages.front().height = ProjectLimits::kMinimumHmiPageHeight - 1;
require(!project.validate(), "an HMI page height of 199 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "an HMI page height of 199 must be rejected");


project = makeValidProject(); project = makeValidProject();
project.controlLogics.front().rungs.front().cells.pop_back(); project.controlLogics.front().rungs.front().cells.pop_back();
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"a ladder row with fewer than ten cells must be rejected"); "a ladder row with fewer than ten cells must be rejected");
} }


@@ -686,19 +688,19 @@ void testEdgeAndCommentBoundaries()
comment_rung.id = "comment-rung"; comment_rung.id = "comment-rung";
comment_rung.name = "Comment rung"; comment_rung.name = "Comment rung";
comment_rung.comment.assign(ProjectLimits::kMaximumRungCommentBytes, 'a'); comment_rung.comment.assign(ProjectLimits::kMaximumRungCommentBytes, 'a');
require(comment_rung.validate(), "a rung comment at the byte limit must be valid");
require(comment_rung.validateStructure(), "a rung comment at the byte limit must be valid");
comment_rung.comment.push_back('a'); comment_rung.comment.push_back('a');
require(!comment_rung.validate(), "a rung comment above the byte limit must fail");
require(!comment_rung.validateStructure(), "a rung comment above the byte limit must fail");
comment_rung.comment = "第一行\n第二行"; comment_rung.comment = "第一行\n第二行";
require(!comment_rung.validate(), "a multiline rung comment must be rejected");
require(!comment_rung.validateStructure(), "a multiline rung comment must be rejected");
comment_rung.comment = "第一行\r第二行"; comment_rung.comment = "第一行\r第二行";
require(!comment_rung.validate(), "a rung comment containing CR must be rejected");
require(!comment_rung.validateStructure(), "a rung comment containing CR must be rejected");


Project project = makeValidProject(); Project project = makeValidProject();
project.registerComments = { project.registerComments = {
{RegisterAddress{RegisterArea::M, 0}, "启动按钮"}, {RegisterAddress{RegisterArea::M, 0}, "启动按钮"},
{RegisterAddress{RegisterArea::M, 0}, "重复地址"}}; {RegisterAddress{RegisterArea::M, 0}, "重复地址"}};
require(!project.validate(), "duplicate register comments must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "duplicate register comments must be rejected");
} }


void testDataInstructionBoundaries() void testDataInstructionBoundaries()
@@ -772,13 +774,13 @@ void testLadderLogicBoundaries()
logic.verticalConnections = { logic.verticalConnections = {
{"vertical-left", "rung-1", "rung-2", 0}, {"vertical-left", "rung-1", "rung-2", 0},
{"vertical-right", "rung-1", "rung-2", 1}}; {"vertical-right", "rung-1", "rung-2", 1}};
require(logic.validate() && logic.validateForRunning(),
require(logic.validate(defaultProjectLimitSettings()) && logic.validateForRunning(defaultProjectLimitSettings()),
"a ten-cell grid with adjacent vertical edges must be valid"); "a ten-cell grid with adjacent vertical edges must be valid");


logic.rungs.front().cells[5].kind = LadderCellKind::Gap; logic.rungs.front().cells[5].kind = LadderCellKind::Gap;
std::string connectivity_error; std::string connectivity_error;
require( require(
logic.validate() && !logic.validateForRunning(&connectivity_error)
logic.validate(defaultProjectLimitSettings()) && !logic.validateForRunning(&connectivity_error)
&& connectivity_error.find("第 1 行") != std::string::npos && connectivity_error.find("第 1 行") != std::string::npos
&& connectivity_error.find("第 6 列") != std::string::npos, && connectivity_error.find("第 6 列") != std::string::npos,
"a disconnected output must report its visual row and break column"); "a disconnected output must report its visual row and break column");
@@ -794,7 +796,7 @@ void testLadderLogicBoundaries()
{"vertical-left", "rung-1", "rung-2", 0}, {"vertical-left", "rung-1", "rung-2", 0},
{"vertical-bypass", "rung-1", "rung-2", 6}}; {"vertical-bypass", "rung-1", "rung-2", 6}};
require( require(
logic.validateForRunning(),
logic.validateForRunning(defaultProjectLimitSettings()),
"a vertical branch that bypasses a gap must keep the output reachable"); "a vertical branch that bypasses a gap must keep the output reachable");


logic.rungs = {upper, lower}; logic.rungs = {upper, lower};
@@ -806,7 +808,7 @@ void testLadderLogicBoundaries()
CoilNodeConfig{ CoilNodeConfig{
RegisterAddress{RegisterArea::M, 2}, CoilMode::Normal}, RegisterAddress{RegisterArea::M, 2}, CoilMode::Normal},
true}; true};
require(!logic.validate(), "a condition cell must reject output nodes");
require(!logic.validate(defaultProjectLimitSettings()), "a condition cell must reject output nodes");


logic.rungs.front() = upper; logic.rungs.front() = upper;
logic.rungs.front().output = LogicNode{ logic.rungs.front().output = LogicNode{
@@ -814,25 +816,25 @@ void testLadderLogicBoundaries()
ContactNodeConfig{ ContactNodeConfig{
RegisterAddress{RegisterArea::M, 2}, ContactMode::NormallyOpen}, RegisterAddress{RegisterArea::M, 2}, ContactMode::NormallyOpen},
true}; true};
require(!logic.validate(), "the output slot must reject condition nodes");
require(!logic.validate(defaultProjectLimitSettings()), "the output slot must reject condition nodes");


logic.rungs.front() = upper; logic.rungs.front() = upper;
logic.rungs.front().output.reset(); logic.rungs.front().output.reset();
require(logic.validate() && logic.validateForRunning(),
require(logic.validate(defaultProjectLimitSettings()) && logic.validateForRunning(defaultProjectLimitSettings()),
"a row without an output may act as a connected branch"); "a row without an output may act as a connected branch");


logic.rungs.front() = upper; logic.rungs.front() = upper;
logic.rungs.front().cells[1].id = logic.rungs.front().cells[0].id; logic.rungs.front().cells[1].id = logic.rungs.front().cells[0].id;
require(!logic.validate(), "cell ids must be unique within a logic");
require(!logic.validate(defaultProjectLimitSettings()), "cell ids must be unique within a logic");


logic.rungs.front() = upper; logic.rungs.front() = upper;
logic.verticalConnections.front().lowerRungId = "missing-rung"; logic.verticalConnections.front().lowerRungId = "missing-rung";
require(!logic.validate(), "vertical edges must reference adjacent rows");
require(!logic.validate(defaultProjectLimitSettings()), "vertical edges must reference adjacent rows");


logic.verticalConnections = { logic.verticalConnections = {
{"vertical-left", "rung-1", "rung-2", 0}, {"vertical-left", "rung-1", "rung-2", 0},
{"vertical-copy", "rung-1", "rung-2", 0}}; {"vertical-copy", "rung-1", "rung-2", 0}};
require(!logic.validate(),
require(!logic.validate(defaultProjectLimitSettings()),
"one row boundary must not contain duplicate vertical edges"); "one row boundary must not contain duplicate vertical edges");
} }


@@ -840,38 +842,38 @@ void testModelsValidateBindingsAndIdentifiers()
{ {
// 聚合验证必须拒绝错误绑定、重复标识和越界控件 // 聚合验证必须拒绝错误绑定、重复标识和越界控件
Project project = makeValidProject(); Project project = makeValidProject();
require(project.validate(), "valid project model must pass validation");
require(project.validate(defaultProjectLimitSettings()), "valid project model must pass validation");


project.hmiPages.front().controls.front().binding = project.hmiPages.front().controls.front().binding =
RegisterAddress{RegisterArea::D, 0}; RegisterAddress{RegisterArea::D, 0};
require(!project.validate(), "button bound to D area must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "button bound to D area must be rejected");


project = makeValidProject(); project = makeValidProject();
project.hmiPages.push_back(project.hmiPages.front()); project.hmiPages.push_back(project.hmiPages.front());
require(!project.validate(), "duplicate HMI page id must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "duplicate HMI page id must be rejected");


project = makeValidProject(); project = makeValidProject();
project.hmiPages.front().controls.front().bounds.x = -1; project.hmiPages.front().controls.front().bounds.x = -1;
require(!project.validate(), "controls outside the page must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "controls outside the page must be rejected");


project = makeValidProject(); project = makeValidProject();
project.hmiPages.front().controls.front().bounds.width = 801; project.hmiPages.front().controls.front().bounds.width = 801;
require(!project.validate(), "controls wider than the page must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "controls wider than the page must be rejected");


project = makeValidProject(); project = makeValidProject();
project.hmiPages.front().controls.front().properties.emplace("", "value"); project.hmiPages.front().controls.front().properties.emplace("", "value");
require(!project.validate(), "empty HMI property names must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "empty HMI property names must be rejected");


project = makeValidProject(); project = makeValidProject();
project.hmiPages.front().controls.front().binding.reset(); project.hmiPages.front().controls.front().binding.reset();
require(project.validate(), "unbound HMI control must be accepted in a draft");
require(!project.validateForRunning(),
require(project.validate(defaultProjectLimitSettings()), "unbound HMI control must be accepted in a draft");
require(!project.validateForRunning(defaultProjectLimitSettings()),
"unbound HMI control must block runtime validation"); "unbound HMI control must block runtime validation");


project = makeValidProject(); project = makeValidProject();
project.controlLogics.front().rungs.front().output->configured = false; project.controlLogics.front().rungs.front().output->configured = false;
require(project.validate(), "unconfigured ladder node must be accepted in a draft");
require(!project.validateForRunning(),
require(project.validate(defaultProjectLimitSettings()), "unconfigured ladder node must be accepted in a draft");
require(!project.validateForRunning(defaultProjectLimitSettings()),
"unconfigured ladder node must block runtime validation"); "unconfigured ladder node must block runtime validation");
} }




+ 14
- 14
app/tests/hmi_editor_service_tests.cpp Parādīt failu

@@ -24,8 +24,8 @@ void testControlEditing()
{ {
// 覆盖控件创建、移动、绑定校验、重命名冲突和删除流程 // 覆盖控件创建、移动、绑定校验、重命名冲突和删除流程
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const HmiEditorResult page_result = service.ensureDefaultPage(); const HmiEditorResult page_result = service.ensureDefaultPage();
require(page_result.succeeded, "default HMI page creation must succeed"); require(page_result.succeeded, "default HMI page creation must succeed");


@@ -190,8 +190,8 @@ void testRuntimeUsesRegisterRepository()
void testHistoryAndAtomicBatchDelete() void testHistoryAndAtomicBatchDelete()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const std::string page_id = service.ensureDefaultPage().id; const std::string page_id = service.ensureDefaultPage().id;
const HmiEditorResult first = service.addControl(page_id, HmiControlType::Label); const HmiEditorResult first = service.addControl(page_id, HmiControlType::Label);
const HmiEditorResult second = service.addControl(page_id, HmiControlType::Label); const HmiEditorResult second = service.addControl(page_id, HmiControlType::Label);
@@ -244,8 +244,8 @@ void testHistoryAndAtomicBatchDelete()
void testBatchPasteControls() void testBatchPasteControls()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const std::string page_id = service.ensureDefaultPage().id; const std::string page_id = service.ensureDefaultPage().id;
const HmiEditorResult first = service.addControl( const HmiEditorResult first = service.addControl(
page_id, HmiControlType::NumericDisplay); page_id, HmiControlType::NumericDisplay);
@@ -290,8 +290,8 @@ void testBatchPasteControls()
void testRuntimeValidationAndPasteBoundaries() void testRuntimeValidationAndPasteBoundaries()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService editor(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService editor(project_service, HmiDefaultSettings{});
const std::string page_id = editor.ensureDefaultPage().id; const std::string page_id = editor.ensureDefaultPage().id;


HmiControl label; HmiControl label;
@@ -366,8 +366,8 @@ void testRuntimeValidationAndPasteBoundaries()
void testAppearanceEditing() void testAppearanceEditing()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const std::string page_id = service.ensureDefaultPage().id; const std::string page_id = service.ensureDefaultPage().id;
const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label); const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label);
require(label.succeeded, "a label must be available for appearance editing"); require(label.succeeded, "a label must be available for appearance editing");
@@ -407,8 +407,8 @@ void testAppearanceEditing()
void testPageLifecycleAndNavigation() void testPageLifecycleAndNavigation()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const std::string main_id = service.ensureDefaultPage().id; const std::string main_id = service.ensureDefaultPage().id;
require(project_service.project().initialHmiPageId == main_id, require(project_service.project().initialHmiPageId == main_id,
"the first page must become the initial HMI page"); "the first page must become the initial HMI page");
@@ -474,8 +474,8 @@ void testPageLifecycleAndNavigation()
void testPageResizeIsAtomicAndUndoable() void testPageResizeIsAtomicAndUndoable()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const std::string page_id = service.ensureDefaultPage().id; const std::string page_id = service.ensureDefaultPage().id;
const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label); const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label);
require(label.succeeded, "page resize fixture must add a control"); require(label.succeeded, "page resize fixture must add a control");


+ 38
- 64
app/tests/logic_editor_service_tests.cpp Parādīt failu

@@ -20,10 +20,22 @@ ContactNodeConfig contact(
return {RegisterAddress{RegisterArea::M, address}, mode}; return {RegisterAddress{RegisterArea::M, address}, mode};
} }


LogicEditorResult setConditionAtColumn(
LogicEditorService &editor,
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured)
{
return editor.applyConditionAndAdvance(
logic_id, {rung_id, column, false}, config, configured).edit;
}

struct Fixture struct Fixture
{ {
TestSupport::InMemoryProjectStorage storage; TestSupport::InMemoryProjectStorage storage;
ProjectService projects{storage};
ProjectService projects{storage, defaultProjectLimitSettings()};
LogicEditorService editor{projects}; LogicEditorService editor{projects};
std::string logicId; std::string logicId;


@@ -109,7 +121,7 @@ void testContinuousGridAndIndependentHorizontalWires()
&& !fixture.editor.canUndo(), && !fixture.editor.canUndo(),
"repeating an existing horizontal wire range must not dirty the project or history"); "repeating an existing horizontal wire range must not dirty the project or history");


const LogicEditorResult node = fixture.editor.setConditionAtColumn(
const LogicEditorResult node = setConditionAtColumn(fixture.editor,
fixture.logicId, rung_id, 3, contact(3), true); fixture.logicId, rung_id, 3, contact(3), true);
require(node.succeeded, "a wire cell must accept a condition node"); require(node.succeeded, "a wire cell must accept a condition node");
require( require(
@@ -211,39 +223,6 @@ void testInsertRowSplitsVerticalEdges()
} }
} }


void testInsertConditionMovesOnlyRelatedVerticalBoundaries()
{
Fixture fixture;
const std::string first = fixture.addRung();
const std::string second = fixture.addRung();
const std::string third = fixture.addRung();
const std::string fourth = fixture.addRung();
require(
fixture.editor.setVerticalConnection(
fixture.logicId, first, second, 2, true).succeeded
&& fixture.editor.setVerticalConnection(
fixture.logicId, first, second, 10, true).succeeded
&& fixture.editor.setVerticalConnection(
fixture.logicId, third, fourth, 5, true).succeeded,
"fixture must create related, output-side, and unrelated edges");

const LogicEditorResult inserted = fixture.editor.insertConditionAtColumn(
fixture.logicId, first, 2, contact(12), true);
require(inserted.succeeded,
"inserting a condition into the grid must succeed");
const ControlLogic *logic = fixture.editor.findLogic(fixture.logicId);
require(
connectionAt(*logic, first, second, 2) == nullptr
&& connectionAt(*logic, first, second, 3) != nullptr,
"an edge at the insertion point must follow shifted grid content");
require(
connectionAt(*logic, first, second, 10) != nullptr,
"the fixed output-side boundary must remain at column ten");
require(
connectionAt(*logic, third, fourth, 5) != nullptr,
"inserting into one row must not move edges in unrelated rows");
}

void testDeleteRowMergesOnlyContinuousEdges() void testDeleteRowMergesOnlyContinuousEdges()
{ {
Fixture fixture; Fixture fixture;
@@ -289,9 +268,9 @@ void testParallelBranchCreatesConnectedVisualRow()
{ {
Fixture fixture; Fixture fixture;
const std::string source = fixture.addRung(); const std::string source = fixture.addRung();
const LogicEditorResult first = fixture.editor.setConditionAtColumn(
const LogicEditorResult first = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 2, contact(1), true); fixture.logicId, source, 2, contact(1), true);
const LogicEditorResult second = fixture.editor.setConditionAtColumn(
const LogicEditorResult second = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 3, contact(2), true); fixture.logicId, source, 3, contact(2), true);
require(first.succeeded && second.succeeded, require(first.succeeded && second.succeeded,
"fixture must place adjacent conditions"); "fixture must place adjacent conditions");
@@ -323,9 +302,9 @@ void testParallelBranchReusesExistingEdges()
Fixture fixture; Fixture fixture;
const std::string source = fixture.addRung(); const std::string source = fixture.addRung();
const std::string following = fixture.addRung(); const std::string following = fixture.addRung();
const LogicEditorResult first = fixture.editor.setConditionAtColumn(
const LogicEditorResult first = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 2, contact(4), true); fixture.logicId, source, 2, contact(4), true);
const LogicEditorResult second = fixture.editor.setConditionAtColumn(
const LogicEditorResult second = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 3, contact(5), true); fixture.logicId, source, 3, contact(5), true);
require(first.succeeded && second.succeeded, require(first.succeeded && second.succeeded,
"fixture must place the branch source conditions"); "fixture must place the branch source conditions");
@@ -358,7 +337,7 @@ void testNodeDeletionAndHistoryAreAtomic()
{ {
Fixture fixture; Fixture fixture;
const std::string rung_id = fixture.addRung(); const std::string rung_id = fixture.addRung();
const LogicEditorResult node = fixture.editor.setConditionAtColumn(
const LogicEditorResult node = setConditionAtColumn(fixture.editor,
fixture.logicId, rung_id, 0, contact(9), true); fixture.logicId, rung_id, 0, contact(9), true);
require(node.succeeded, "fixture must place a condition"); require(node.succeeded, "fixture must place a condition");
fixture.editor.clearHistory(); fixture.editor.clearHistory();
@@ -380,7 +359,7 @@ void testNodeDeletionAndHistoryAreAtomic()
} }
require(fixture.editor.redo().succeeded, "the drag must redo atomically"); require(fixture.editor.redo().succeeded, "the drag must redo atomically");
require( require(
fixture.editor.removeNode(fixture.logicId, node.id).succeeded,
fixture.editor.removeNodes(fixture.logicId, {node.id}).succeeded,
"deleting a condition node must succeed"); "deleting a condition node must succeed");
rung = fixture.editor.findRung(fixture.logicId, rung_id); rung = fixture.editor.findRung(fixture.logicId, rung_id);
require( require(
@@ -406,7 +385,7 @@ void testSelectionDeletionIsAtomic()
const std::string upper = fixture.addRung(); const std::string upper = fixture.addRung();
const std::string lower = fixture.addRung(); const std::string lower = fixture.addRung();
require( require(
fixture.editor.setConditionAtColumn(
setConditionAtColumn(fixture.editor,
fixture.logicId, upper, 0, contact(30), true).succeeded fixture.logicId, upper, 0, contact(30), true).succeeded
&& fixture.editor.setHorizontalWireRange( && fixture.editor.setHorizontalWireRange(
fixture.logicId, upper, 1, 1, true).succeeded fixture.logicId, upper, 1, 1, true).succeeded
@@ -470,7 +449,7 @@ void testInvalidSelectionDeletionDoesNotMutateOrRecordHistory()
Fixture fixture; Fixture fixture;
const std::string rung_id = fixture.addRung(); const std::string rung_id = fixture.addRung();
require( require(
fixture.editor.setConditionAtColumn(
setConditionAtColumn(fixture.editor,
fixture.logicId, rung_id, 0, contact(40), true).succeeded fixture.logicId, rung_id, 0, contact(40), true).succeeded
&& fixture.editor.setHorizontalWireRange( && fixture.editor.setHorizontalWireRange(
fixture.logicId, rung_id, 1, 1, true).succeeded, fixture.logicId, rung_id, 1, 1, true).succeeded,
@@ -564,11 +543,9 @@ void testProjectRungLimitAppliesToBranchAndPaste()
{ {
Fixture fixture; Fixture fixture;
const std::string source = fixture.addRung(); const std::string source = fixture.addRung();
const LogicEditorResult node = fixture.editor.setConditionAtColumn(
const LogicEditorResult node = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 0, contact(20), true); fixture.logicId, source, 0, contact(20), true);
require(node.succeeded, "fixture must create a branch source node"); require(node.succeeded, "fixture must create a branch source node");
const LadderRung source_copy = *fixture.editor.findRung(
fixture.logicId, source);


Project &project = fixture.projects.editProject(); Project &project = fixture.projects.editProject();
std::size_t remaining = ProjectLimits::kMaximumRungsPerProject - 1U; std::size_t remaining = ProjectLimits::kMaximumRungsPerProject - 1U;
@@ -601,16 +578,13 @@ void testProjectRungLimitAppliesToBranchAndPaste()
remaining -= count; remaining -= count;
++logic_index; ++logic_index;
} }
require(project.validate(),
require(project.validate(defaultProjectLimitSettings()),
"the project-wide rung limit fixture must itself be valid"); "the project-wide rung limit fixture must itself be valid");


require( require(
!fixture.editor.addParallelBranch( !fixture.editor.addParallelBranch(
fixture.logicId, source, {node.id}, contact(21), true).succeeded, fixture.logicId, source, {node.id}, contact(21), true).succeeded,
"parallel insertion must respect the project-wide rung limit"); "parallel insertion must respect the project-wide rung limit");
require(
!fixture.editor.pasteRung(fixture.logicId, source_copy).succeeded,
"row paste must respect the project-wide rung limit");
require( require(
fixture.editor.findLogic(fixture.logicId)->rungs.size() == 1U, fixture.editor.findLogic(fixture.logicId)->rungs.size() == 1U,
"failed limit checks must not partially add a row"); "failed limit checks must not partially add a row");
@@ -635,9 +609,9 @@ void testFirstEditOnEmptyLogicIsAtomic()
Fixture fixture; Fixture fixture;
fixture.editor.clearHistory(); fixture.editor.clearHistory();


const LogicEditorResult condition = fixture.editor.appendCondition(
const LogicEditResult condition = fixture.editor.applyConditionAndAdvance(
fixture.logicId, {}, contact(11), true); fixture.logicId, {}, contact(11), true);
require(condition.succeeded,
require(condition.edit.succeeded,
"placing the first condition must create the first row"); "placing the first condition must create the first row");
const ControlLogic *logic = fixture.editor.findLogic(fixture.logicId); const ControlLogic *logic = fixture.editor.findLogic(fixture.logicId);
require(logic->rungs.size() == 1U require(logic->rungs.size() == 1U
@@ -653,10 +627,11 @@ void testFirstEditOnEmptyLogicIsAtomic()
"redo must restore the atomically created first condition and row"); "redo must restore the atomically created first condition and row");


fixture.editor.clearHistory(); fixture.editor.clearHistory();
const LogicEditorResult wire = fixture.editor.appendWire(
fixture.logicId, {}, 1);
require(wire.succeeded,
"appending a wire to an empty logic must create a first row");
const std::string wire_rung = fixture.addRung();
const LogicEditResult wire = fixture.editor.applyWireAndAdvance(
fixture.logicId, {wire_rung, 9, false});
require(wire.edit.succeeded,
"a wire edit must succeed on a newly added row");
logic = fixture.editor.findLogic(fixture.logicId); logic = fixture.editor.findLogic(fixture.logicId);
require(logic->rungs.size() == 2U require(logic->rungs.size() == 2U
&& logic->rungs.back().cells.back().kind == LadderCellKind::Wire, && logic->rungs.back().cells.back().kind == LadderCellKind::Wire,
@@ -759,9 +734,9 @@ void testOutputAutomaticallyCompletesTrailingWires()
Fixture trailing; Fixture trailing;
const std::string rung_id = trailing.addRung(); const std::string rung_id = trailing.addRung();
require( require(
trailing.editor.setConditionAtColumn(
setConditionAtColumn(trailing.editor,
trailing.logicId, rung_id, 0, contact(55), true).succeeded trailing.logicId, rung_id, 0, contact(55), true).succeeded
&& trailing.editor.setConditionAtColumn(
&& setConditionAtColumn(trailing.editor,
trailing.logicId, rung_id, 2, contact(56), true).succeeded, trailing.logicId, rung_id, 2, contact(56), true).succeeded,
"the trailing-wire fixture must leave one intentional middle gap"); "the trailing-wire fixture must leave one intentional middle gap");
trailing.editor.clearHistory(); trailing.editor.clearHistory();
@@ -888,9 +863,9 @@ void testMixedAndSparseGridClipboardFragments()
Fixture fixture; Fixture fixture;
const std::string source = fixture.addRung(); const std::string source = fixture.addRung();
const std::string target = fixture.addRung(); const std::string target = fixture.addRung();
const LogicEditorResult first = fixture.editor.setConditionAtColumn(
const LogicEditorResult first = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 0, contact(70), true); fixture.logicId, source, 0, contact(70), true);
const LogicEditorResult second = fixture.editor.setConditionAtColumn(
const LogicEditorResult second = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 2, contact(71), true); fixture.logicId, source, 2, contact(71), true);
require( require(
first.succeeded && second.succeeded first.succeeded && second.succeeded
@@ -941,7 +916,7 @@ void testMixedAndSparseGridClipboardFragments()
require( require(
sparse.editor.setHorizontalWireRange( sparse.editor.setHorizontalWireRange(
sparse.logicId, sparse_source, 0, 0, true).succeeded sparse.logicId, sparse_source, 0, 0, true).succeeded
&& sparse.editor.setConditionAtColumn(
&& setConditionAtColumn(sparse.editor,
sparse.logicId, sparse_source, 2, contact(72), true).succeeded sparse.logicId, sparse_source, 2, contact(72), true).succeeded
&& sparse.editor.setHorizontalWireRange( && sparse.editor.setHorizontalWireRange(
sparse.logicId, sparse_target, 5, 5, true).succeeded, sparse.logicId, sparse_target, 5, 5, true).succeeded,
@@ -973,7 +948,7 @@ void testGridClipboardFailuresAreAtomic()
require( require(
fixture.editor.setHorizontalWireRange( fixture.editor.setHorizontalWireRange(
fixture.logicId, source, 0, 1, true).succeeded fixture.logicId, source, 0, 1, true).succeeded
&& fixture.editor.setConditionAtColumn(
&& setConditionAtColumn(fixture.editor,
fixture.logicId, target, 4, contact(73), true).succeeded fixture.logicId, target, 4, contact(73), true).succeeded
&& fixture.editor.setHorizontalWireRange( && fixture.editor.setHorizontalWireRange(
fixture.logicId, target, 0, 0, true).succeeded, fixture.logicId, target, 0, 0, true).succeeded,
@@ -1168,7 +1143,7 @@ void testWholeRowClipboardInsertionAndLimit()
const std::string first = fixture.addRung(); const std::string first = fixture.addRung();
const std::string second = fixture.addRung(); const std::string second = fixture.addRung();
const std::string target = fixture.addRung(); const std::string target = fixture.addRung();
const LogicEditorResult source_node = fixture.editor.setConditionAtColumn(
const LogicEditorResult source_node = setConditionAtColumn(fixture.editor,
fixture.logicId, first, 0, contact(90), true); fixture.logicId, first, 0, contact(90), true);
require( require(
source_node.succeeded source_node.succeeded
@@ -1439,7 +1414,6 @@ int main()
testContinuousGridAndIndependentHorizontalWires(); testContinuousGridAndIndependentHorizontalWires();
testIndependentVerticalConnectionsAndNetworkSplit(); testIndependentVerticalConnectionsAndNetworkSplit();
testInsertRowSplitsVerticalEdges(); testInsertRowSplitsVerticalEdges();
testInsertConditionMovesOnlyRelatedVerticalBoundaries();
testDeleteRowMergesOnlyContinuousEdges(); testDeleteRowMergesOnlyContinuousEdges();
testParallelBranchCreatesConnectedVisualRow(); testParallelBranchCreatesConnectedVisualRow();
testParallelBranchReusesExistingEdges(); testParallelBranchReusesExistingEdges();


+ 0
- 1
app/tests/offline_simulation_service_tests.cpp Parādīt failu

@@ -747,7 +747,6 @@ void testSimulationLifecycleSnapshotAndFault()
require(simulation.start({program}).succeeded, "simulation must start"); require(simulation.start({program}).succeeded, "simulation must start");
require(simulation.state() == SimulationState::Running, "simulation must report running"); require(simulation.state() == SimulationState::Running, "simulation must report running");
require(!readBit(repository, 0), "starting a session must clear virtual registers"); require(!readBit(repository, 0), "starting a session must clear virtual registers");
require(simulation.scanIntervalMs() == 50, "default scan interval must be 50 ms");
writeBit(repository, 0, true); writeBit(repository, 0, true);
program.rungs.front().output = coil("changed-output", 9); program.rungs.front().output = coil("changed-output", 9);
require(simulation.executeOnce().succeeded, "manual lifecycle scan must succeed"); require(simulation.executeOnce().succeeded, "manual lifecycle scan must succeed");


+ 15
- 11
app/tests/plc_runtime_tests.cpp Parādīt failu

@@ -64,6 +64,15 @@ public:
return {true, {}}; return {true, {}};
} }


PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses,
const std::vector<RegisterWordRange> &ranges) override
{
poll_addresses = addresses;
poll_ranges = ranges;
return {true, {}};
}

PlcConnectionState state() const override { return connection_state; } PlcConnectionState state() const override { return connection_state; }
bool initialReadCompleted() const override { return initial_read; } bool initialReadCompleted() const override { return initial_read; }
PlcCommunicationError lastErrorType() const override { return last_error_type; } PlcCommunicationError lastErrorType() const override { return last_error_type; }
@@ -120,6 +129,7 @@ public:
std::string last_error; std::string last_error;
PlcSerialConfiguration last_configuration; PlcSerialConfiguration last_configuration;
std::vector<RegisterAddress> poll_addresses; std::vector<RegisterAddress> poll_addresses;
std::vector<RegisterWordRange> poll_ranges;
std::function<void()> state_changed; std::function<void()> state_changed;
std::function<void(bool)> initial_read_changed; std::function<void(bool)> initial_read_changed;
std::function<void()> cache_updated; std::function<void()> cache_updated;
@@ -211,14 +221,14 @@ void testPlcCacheAndWriteForwarding()
&& multi_word_write_count == 2, && multi_word_write_count == 2,
"Double words must be forwarded in one four-register request"); "Double words must be forwarded in one four-register request");
repository.invalidate(); repository.invalidate();
require(!repository.hasAnyValidValue(),
"disconnecting must invalidate all PLC cache validity flags");
require(!repository.readWord(d0).succeeded,
"disconnecting must invalidate cached PLC values");
} }


void testRuntimeRepositorySwitchingAndDisconnect() void testRuntimeRepositorySwitchingAndDisconnect()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiPage page; HmiPage page;
page.id = "main-page"; page.id = "main-page";
page.name = "Main"; page.name = "Main";
@@ -287,7 +297,7 @@ void testRuntimeRepositorySwitchingAndDisconnect()
void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect() void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository; VirtualRegisterRepository virtual_repository;
PlcRegisterRepository plc_repository; PlcRegisterRepository plc_repository;
ActiveRegisterRepository active_repository(virtual_repository); ActiveRegisterRepository active_repository(virtual_repository);
@@ -502,13 +512,7 @@ void testPlcPollQuantityBoundaries()
} }
require(service.setPollAddresses( require(service.setPollAddresses(
double_boundary_addresses, double_boundary_addresses,
{{RegisterAddress{RegisterArea::D, 118}, 4}}).succeeded
&& service.pollBlocks().size() == 2U
&& service.pollBlocks()[0].area == RegisterArea::D
&& service.pollBlocks()[0].startAddress == 0
&& service.pollBlocks()[0].count == 118
&& service.pollBlocks()[1].startAddress == 118
&& service.pollBlocks()[1].count == 6,
{{RegisterAddress{RegisterArea::D, 118}, 4}}).succeeded,
"a Double at the 120-word boundary must stay in one Modbus read block"); "a Double at the 120-word boundary must stay in one Modbus read block");


std::vector<RegisterWordRange> unsplittable_ranges; std::vector<RegisterWordRange> unsplittable_ranges;


+ 12
- 12
app/tests/project_management_tests.cpp Parādīt failu

@@ -163,8 +163,8 @@ void testEmptyProjectRoundTrip()
{ {
QTemporaryDir directory; QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid"); require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
ProjectService service(storage);
JsonProjectStorage storage(defaultProjectLimitSettings());
ProjectService service(storage, defaultProjectLimitSettings());
require(service.createNewProject("Empty project").succeeded, require(service.createNewProject("Empty project").succeeded,
"empty project creation must succeed"); "empty project creation must succeed");
require(service.project().metadata.formatVersion == "2.0", require(service.project().metadata.formatVersion == "2.0",
@@ -184,10 +184,10 @@ void testGridProjectRoundTrip()
{ {
QTemporaryDir directory; QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid"); require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
const QString path = directory.filePath("grid.json"); const QString path = directory.filePath("grid.json");
const Project original = makeExampleProject(); const Project original = makeExampleProject();
require(original.validate(), "the grid fixture must be structurally valid");
require(original.validate(defaultProjectLimitSettings()), "the grid fixture must be structurally valid");
require(storage.save(original, path.toStdString()).succeeded, require(storage.save(original, path.toStdString()).succeeded,
"the grid project must save"); "the grid project must save");


@@ -225,7 +225,7 @@ void testMOffAlarmRoundTrip()
{ {
QTemporaryDir directory; QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid"); require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
const QString path = directory.filePath("m-off-alarm.json"); const QString path = directory.filePath("m-off-alarm.json");
Project project = makeExampleProject(); Project project = makeExampleProject();
project.alarmDefinitions.push_back({ project.alarmDefinitions.push_back({
@@ -254,7 +254,7 @@ void testMultiWordHmiDataTypeRoundTrip()
{ {
QTemporaryDir directory; QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid"); require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
const QString path = directory.filePath("multi-word-hmi.json"); const QString path = directory.filePath("multi-word-hmi.json");
Project project = makeExampleProject(); Project project = makeExampleProject();


@@ -317,7 +317,7 @@ void testStrictVersionAndRequiredFields()
{ {
QTemporaryDir directory; QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid"); require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
const QString fixture_path = directory.filePath("fixture.json"); const QString fixture_path = directory.filePath("fixture.json");
QJsonObject root = savedFixture( QJsonObject root = savedFixture(
storage, makeExampleProject(), fixture_path); storage, makeExampleProject(), fixture_path);
@@ -346,7 +346,7 @@ void testInvalidGridAndConnectionsAreRejected()
{ {
QTemporaryDir directory; QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid"); require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
const QString fixture_path = directory.filePath("fixture.json"); const QString fixture_path = directory.filePath("fixture.json");
const QJsonObject original = savedFixture( const QJsonObject original = savedFixture(
storage, makeExampleProject(), fixture_path); storage, makeExampleProject(), fixture_path);
@@ -417,7 +417,7 @@ void testProjectServiceStateAndConfiguredLimits()
QTemporaryDir directory; QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid"); require(directory.isValid(), "temporary directory must be valid");
const QString path = directory.filePath("project.json"); const QString path = directory.filePath("project.json");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
require(storage.save(makeExampleProject(), path.toStdString()).succeeded, require(storage.save(makeExampleProject(), path.toStdString()).succeeded,
"service fixture must save"); "service fixture must save");


@@ -435,7 +435,7 @@ void testProjectServiceStateAndConfiguredLimits()
&& !limited_service.hasCurrentFile(), && !limited_service.hasCurrentFile(),
"a failed load must leave the current project untouched"); "a failed load must leave the current project untouched");


ProjectService service(storage);
ProjectService service(storage, defaultProjectLimitSettings());
require(service.save().error == ProjectServiceError::FilePathRequired, require(service.save().error == ProjectServiceError::FilePathRequired,
"save without a current path must fail explicitly"); "save without a current path must fail explicitly");
require(service.createNewProject("State project").succeeded, require(service.createNewProject("State project").succeeded,
@@ -450,8 +450,8 @@ void testProjectServiceStateAndConfiguredLimits()


void testRegisterCommentService() void testRegisterCommentService()
{ {
JsonProjectStorage storage;
ProjectService projects(storage);
JsonProjectStorage storage(defaultProjectLimitSettings());
ProjectService projects(storage, defaultProjectLimitSettings());
RegisterCommentService comments(projects); RegisterCommentService comments(projects);
require(comments.setComment( require(comments.setComment(
RegisterAddress{RegisterArea::D, 3}, " Target value ") RegisterAddress{RegisterArea::D, 3}, " Target value ")


+ 18
- 10
app/tests/register_monitor_service_tests.cpp Parādīt failu

@@ -88,20 +88,24 @@ void testRegisterWrites()
require(bit_write.succeeded && repository.readBit(m0).value, require(bit_write.succeeded && repository.readBit(m0).value,
"monitor service must write M values through the active repository"); "monitor service must write M values through the active repository");


const RegisterMonitorWriteResult word_write = service.writeWord(d0, -123);
const RegisterMonitorWriteResult word_write = service.writeNumeric(
d0, RegisterDataType::Int16, -123);
require(word_write.succeeded && repository.readWord(d0).value == -123, require(word_write.succeeded && repository.readWord(d0).value == -123,
"monitor service must write signed D values through the active repository"); "monitor service must write signed D values through the active repository");


const RegisterMonitorWriteResult wrong_bit = service.writeBit(d0, true); const RegisterMonitorWriteResult wrong_bit = service.writeBit(d0, true);
require(!wrong_bit.succeeded && wrong_bit.error == RegisterError::AreaMismatch, require(!wrong_bit.succeeded && wrong_bit.error == RegisterError::AreaMismatch,
"monitor bit writes must reject D addresses"); "monitor bit writes must reject D addresses");
const RegisterMonitorWriteResult wrong_word = service.writeWord(m0, 123);
const RegisterMonitorWriteResult wrong_word = service.writeNumeric(
m0, RegisterDataType::Int16, 123);
require(!wrong_word.succeeded && wrong_word.error == RegisterError::AreaMismatch, require(!wrong_word.succeeded && wrong_word.error == RegisterError::AreaMismatch,
"monitor word writes must reject M addresses"); "monitor word writes must reject M addresses");


require(service.writeWord(d0, static_cast<std::int16_t>(-32768)).succeeded
require(service.writeNumeric(
d0, RegisterDataType::Int16, static_cast<std::int16_t>(-32768)).succeeded
&& repository.readWord(d0).value == -32768 && repository.readWord(d0).value == -32768
&& service.writeWord(d0, static_cast<std::int16_t>(32767)).succeeded
&& service.writeNumeric(
d0, RegisterDataType::Int16, static_cast<std::int16_t>(32767)).succeeded
&& repository.readWord(d0).value == 32767, && repository.readWord(d0).value == 32767,
"monitor word writes must preserve signed 16-bit boundary values"); "monitor word writes must preserve signed 16-bit boundary values");
} }
@@ -116,8 +120,10 @@ void testOfflineInitialValueCapture()
service.setOfflineInitialCaptureEnabled(true); service.setOfflineInitialCaptureEnabled(true);


require(service.writeBit({RegisterArea::M, 3}, true).succeeded require(service.writeBit({RegisterArea::M, 3}, true).succeeded
&& service.writeWord({RegisterArea::D, 10}, -456).succeeded
&& service.writeFloat({RegisterArea::D, 20}, 1.5f).succeeded
&& service.writeNumeric(
{RegisterArea::D, 10}, RegisterDataType::Int16, -456).succeeded
&& service.writeNumeric(
{RegisterArea::D, 20}, RegisterDataType::Float32, 1.5f).succeeded
&& service.writeNumeric( && service.writeNumeric(
{RegisterArea::D, 30}, RegisterDataType::Int32, 305419896) {RegisterArea::D, 30}, RegisterDataType::Int32, 305419896)
.succeeded .succeeded
@@ -132,8 +138,8 @@ void testOfflineInitialValueCapture()
require(initial_repository.readBit({RegisterArea::M, 3}).value require(initial_repository.readBit({RegisterArea::M, 3}).value
&& initial_repository.readWord({RegisterArea::D, 10}).value == -456 && initial_repository.readWord({RegisterArea::D, 10}).value == -456
&& std::fabs(Float32Codec::decode( && std::fabs(Float32Codec::decode(
initial_repository.readWordPair({RegisterArea::D, 20}).values[0],
initial_repository.readWordPair({RegisterArea::D, 20}).values[1])
initial_repository.readWords({RegisterArea::D, 20}, 2).values[0],
initial_repository.readWords({RegisterArea::D, 20}, 2).values[1])
.value_or(0.0f) - 1.5f) < 0.000001f, .value_or(0.0f) - 1.5f) < 0.000001f,
"editing monitor writes must be captured as offline initial values"); "editing monitor writes must be captured as offline initial values");
require(captured_int32.succeeded require(captured_int32.succeeded
@@ -182,7 +188,8 @@ void testMultiWordMonitoring()
&& service.points().size() == 3U, && service.points().size() == 3U,
"overlapping monitor removal must identify the point type"); "overlapping monitor removal must identify the point type");


require(service.writeFloat({RegisterArea::D, 1}, -2.5f).succeeded,
require(service.writeNumeric(
{RegisterArea::D, 1}, RegisterDataType::Float32, -2.5f).succeeded,
"Float32 monitor writes must use the pair write path"); "Float32 monitor writes must use the pair write path");
std::vector<MonitorValue> values = service.values(false); std::vector<MonitorValue> values = service.values(false);
require(values.front().state == MonitorValueState::Valid require(values.front().state == MonitorValueState::Valid
@@ -190,7 +197,8 @@ void testMultiWordMonitoring()
std::get<float>(values.front().numericValue) - (-2.5f)) std::get<float>(values.front().numericValue) - (-2.5f))
< 0.000001f, < 0.000001f,
"Float32 monitor values must decode the pair correctly"); "Float32 monitor values must decode the pair correctly");
require(!service.writeFloat({RegisterArea::D, 4000}, 1.0f).succeeded,
require(!service.writeNumeric(
{RegisterArea::D, 4000}, RegisterDataType::Float32, 1.0f).succeeded,
"Float32 monitor writes at D4000 must be rejected"); "Float32 monitor writes at D4000 must be rejected");


const RegisterMonitorResult int32_added = service.addRange( const RegisterMonitorResult int32_added = service.addRange(


+ 3
- 3
app/tests/runtime_mode_service_tests.cpp Parādīt failu

@@ -138,7 +138,7 @@ void testModeTransitions()
{ {
// 验证服务将 PLC 首读状态与领域模式切换规则正确组合 // 验证服务将 PLC 首读状态与领域模式切换规则正确组合
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository; VirtualRegisterRepository virtual_repository;
VirtualRegisterRepository plc_repository; VirtualRegisterRepository plc_repository;
ActiveRegisterRepository active_repository(virtual_repository); ActiveRegisterRepository active_repository(virtual_repository);
@@ -325,7 +325,7 @@ void testModeTransitions()
void testMonitorPollRangeRollback() void testMonitorPollRangeRollback()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository; VirtualRegisterRepository virtual_repository;
VirtualRegisterRepository plc_repository; VirtualRegisterRepository plc_repository;
ActiveRegisterRepository active_repository(virtual_repository); ActiveRegisterRepository active_repository(virtual_repository);
@@ -359,7 +359,7 @@ void testMonitorPollRangeRollback()
void testDisconnectedOutputBlocksOfflineAndOnlineRuntime() void testDisconnectedOutputBlocksOfflineAndOnlineRuntime()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository; VirtualRegisterRepository virtual_repository;
VirtualRegisterRepository plc_repository; VirtualRegisterRepository plc_repository;
ActiveRegisterRepository active_repository(virtual_repository); ActiveRegisterRepository active_repository(virtual_repository);


+ 44
- 19
app/tests/runtime_panel_controller_tests.cpp Parādīt failu

@@ -42,6 +42,18 @@


namespace { namespace {


LogicEditorResult setConditionAtColumn(
LogicEditorService &editor,
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured)
{
return editor.applyConditionAndAdvance(
logic_id, {rung_id, column, false}, config, configured).edit;
}

using TestProjectStorage = TestSupport::InMemoryProjectStorage; using TestProjectStorage = TestSupport::InMemoryProjectStorage;
using TestSupport::require; using TestSupport::require;


@@ -98,7 +110,7 @@ void testMonitorOffersAllNumericTypes()
void testAlarmConfigurationOffersMOnAndMOff() void testAlarmConfigurationOffersMOnAndMOff()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
AlarmEditorService alarm_editor_service(project_service); AlarmEditorService alarm_editor_service(project_service);
AlarmConfigurationDialog dialog(alarm_editor_service); AlarmConfigurationDialog dialog(alarm_editor_service);


@@ -123,9 +135,9 @@ void testAlarmConfigurationOffersMOnAndMOff()
void testAlarmListKeepsFixedGeometryWhileRecordsChange() void testAlarmListKeepsFixedGeometryWhileRecordsChange()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository; VirtualRegisterRepository virtual_repository;
HmiEditorService hmi_editor_service(project_service);
HmiEditorService hmi_editor_service(project_service, HmiDefaultSettings{});
HmiRuntimeService hmi_runtime_service(virtual_repository); HmiRuntimeService hmi_runtime_service(virtual_repository);
AlarmService alarm_service(project_service, virtual_repository); AlarmService alarm_service(project_service, virtual_repository);


@@ -195,7 +207,7 @@ void testAlarmListKeepsFixedGeometryWhileRecordsChange()
void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing() void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository; VirtualRegisterRepository virtual_repository;
OfflineSimulationService simulation_service(virtual_repository); OfflineSimulationService simulation_service(virtual_repository);
OnlineLogicMonitorService online_monitor_service(virtual_repository); OnlineLogicMonitorService online_monitor_service(virtual_repository);
@@ -205,7 +217,7 @@ void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing()
logic_editor_service, logic_editor_service,
simulation_service, simulation_service,
online_monitor_service); online_monitor_service);
HmiEditorService hmi_editor_service(project_service);
HmiEditorService hmi_editor_service(project_service, HmiDefaultSettings{});
HmiRuntimeService hmi_runtime_service(virtual_repository); HmiRuntimeService hmi_runtime_service(virtual_repository);
HmiNavigationService hmi_navigation_service(project_service); HmiNavigationService hmi_navigation_service(project_service);
AlarmService alarm_service(project_service, virtual_repository); AlarmService alarm_service(project_service, virtual_repository);
@@ -301,18 +313,31 @@ void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing()
controller.leaveRuntime( controller.leaveRuntime(
runtime_mode_service.mode(), runtime_mode_service.plcConnectionState()); runtime_mode_service.mode(), runtime_mode_service.plcConnectionState());
logic_editor_widget.clearRuntimeTrace(); logic_editor_widget.clearRuntimeTrace();
require(!logic_editor_widget.runtimeTraceEnabled(),
const auto runtimeOutputIsActive = [runtime_logic_view]
{
for (QGraphicsItem *item : runtime_logic_view->scene()->items())
{
if (item->data(0).toString() == QStringLiteral("output")
&& item->data(1).toString()
== QStringLiteral("always-on-rung"))
{
return item->data(3).toBool() && item->data(4).toBool();
}
}
return false;
};
require(!runtimeOutputIsActive(),
"editing transition must initially clear the runtime trace"); "editing transition must initially clear the runtime trace");


QCoreApplication::processEvents(QEventLoop::AllEvents); QCoreApplication::processEvents(QEventLoop::AllEvents);
require(!logic_editor_widget.runtimeTraceEnabled(),
require(!runtimeOutputIsActive(),
"a queued offline scan must not restore the trace after returning to editing"); "a queued offline scan must not restore the trace after returning to editing");
} }


void testLogicEditorGridSelectionAndDeletion() void testLogicEditorGridSelectionAndDeletion()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService logic_editor_service(project_service); LogicEditorService logic_editor_service(project_service);
const std::string logic_id = logic_editor_service.ensureDefaultLogic().id; const std::string logic_id = logic_editor_service.ensureDefaultLogic().id;


@@ -441,7 +466,7 @@ void testLogicEditorGridSelectionAndDeletion()
== LadderCellKind::Wire, == LadderCellKind::Wire,
"the horizontal-wire command must act on an explicitly selected cell"); "the horizontal-wire command must act on an explicitly selected cell");


const LogicEditorResult first_node = logic_editor_service.setConditionAtColumn(
const LogicEditorResult first_node = setConditionAtColumn(logic_editor_service,
logic_id, logic_id,
rung_id, rung_id,
0, 0,
@@ -449,7 +474,7 @@ void testLogicEditorGridSelectionAndDeletion()
RegisterAddress{RegisterArea::M, 0}, RegisterAddress{RegisterArea::M, 0},
ContactMode::NormallyOpen}, ContactMode::NormallyOpen},
true); true);
const LogicEditorResult second_node = logic_editor_service.setConditionAtColumn(
const LogicEditorResult second_node = setConditionAtColumn(logic_editor_service,
logic_id, logic_id,
rung_id, rung_id,
1, 1,
@@ -509,13 +534,13 @@ void testLogicEditorGridSelectionAndDeletion()
void testLadderLayoutAndDragDeletion() void testLadderLayoutAndDragDeletion()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service); LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id; const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string upper = editor.addRung(logic_id).id; const std::string upper = editor.addRung(logic_id).id;
const std::string lower = editor.addRung(logic_id).id; const std::string lower = editor.addRung(logic_id).id;
require( require(
editor.setConditionAtColumn(
setConditionAtColumn(editor,
logic_id, logic_id,
upper, upper,
0, 0,
@@ -688,7 +713,7 @@ void testLadderLayoutAndDragDeletion()
void testWireGesturePreviewKeepsAtomicCommit() void testWireGesturePreviewKeepsAtomicCommit()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service); LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id; const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string first = editor.addRung(logic_id).id; const std::string first = editor.addRung(logic_id).id;
@@ -842,7 +867,7 @@ void testWireGesturePreviewKeepsAtomicCommit()
void testEscapeExitsMouseWireMode() void testEscapeExitsMouseWireMode()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service); LogicEditorService editor(project_service);
LogicEditorWidget widget(editor); LogicEditorWidget widget(editor);


@@ -872,7 +897,7 @@ void testEscapeExitsMouseWireMode()
void testCursorAdvanceAndInlineCommandInput() void testCursorAdvanceAndInlineCommandInput()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service); LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id; const std::string logic_id = editor.ensureDefaultLogic().id;
LogicEditorWidget widget(editor); LogicEditorWidget widget(editor);
@@ -970,7 +995,7 @@ void testCursorAdvanceAndInlineCommandInput()
void testVerticalWireShortcutAdvancesDownward() void testVerticalWireShortcutAdvancesDownward()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service); LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id; const std::string logic_id = editor.ensureDefaultLogic().id;
std::vector<std::string> rung_ids; std::vector<std::string> rung_ids;
@@ -1059,12 +1084,12 @@ void testVerticalWireShortcutAdvancesDownward()
void testSegmentLevelTraceProjection() void testSegmentLevelTraceProjection()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service); LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id; const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string upper = editor.addRung(logic_id).id; const std::string upper = editor.addRung(logic_id).id;
const std::string lower = editor.addRung(logic_id).id; const std::string lower = editor.addRung(logic_id).id;
const LogicEditorResult condition = editor.setConditionAtColumn(
const LogicEditorResult condition = setConditionAtColumn(editor,
logic_id, logic_id,
upper, upper,
0, 0,
@@ -1157,7 +1182,7 @@ void testSegmentLevelTraceProjection()
void testLogicClipboardUsesExplicitObjectAndRowSelection() void testLogicClipboardUsesExplicitObjectAndRowSelection()
{ {
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service); LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id; const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string source = editor.addRung(logic_id).id; const std::string source = editor.addRung(logic_id).id;


Notiek ielāde…
Atcelt
Saglabāt