| @@ -140,17 +140,8 @@ void testConfiguredAlarmLimit() | |||||
| int main() | int main() | ||||
| { | { | ||||
| try | |||||
| { | |||||
| testAlarmDefinitionsAndRuntimeLifecycle(); | |||||
| testConfiguredAlarmLimit(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "alarm service tests failed: " << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "alarm service tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("alarm service tests", { | |||||
| {"testAlarmDefinitionsAndRuntimeLifecycle", testAlarmDefinitionsAndRuntimeLifecycle}, | |||||
| {"testConfiguredAlarmLimit", testConfiguredAlarmLimit}, | |||||
| }); | |||||
| } | } | ||||
| @@ -232,21 +232,12 @@ void testCreationFailureKeepsTheApplicationStartable() | |||||
| int main(int argc, char *argv[]) | int main(int argc, char *argv[]) | ||||
| { | { | ||||
| QCoreApplication application(argc, argv); | QCoreApplication application(argc, argv); | ||||
| try | |||||
| { | |||||
| testMissingFileCreatesDefaults(); | |||||
| testCompleteConfigurationLoadsEveryPublicField(); | |||||
| testMissingAndUnknownFieldsDoNotInvalidateConfiguration(); | |||||
| testDuplicateOrInvalidFieldsRejectTheWholeFile(); | |||||
| testVersionAndEncodingErrorsRejectTheWholeFile(); | |||||
| testCreationFailureKeepsTheApplicationStartable(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "application settings tests failed: " | |||||
| << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "application settings tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("application settings tests", { | |||||
| {"testMissingFileCreatesDefaults", testMissingFileCreatesDefaults}, | |||||
| {"testCompleteConfigurationLoadsEveryPublicField", testCompleteConfigurationLoadsEveryPublicField}, | |||||
| {"testMissingAndUnknownFieldsDoNotInvalidateConfiguration", testMissingAndUnknownFieldsDoNotInvalidateConfiguration}, | |||||
| {"testDuplicateOrInvalidFieldsRejectTheWholeFile", testDuplicateOrInvalidFieldsRejectTheWholeFile}, | |||||
| {"testVersionAndEncodingErrorsRejectTheWholeFile", testVersionAndEncodingErrorsRejectTheWholeFile}, | |||||
| {"testCreationFailureKeepsTheApplicationStartable", testCreationFailureKeepsTheApplicationStartable}, | |||||
| }); | |||||
| } | } | ||||
| @@ -1120,34 +1120,24 @@ void testRuntimeConfiguredProjectLimits() | |||||
| int main() | int main() | ||||
| { | { | ||||
| try | |||||
| { | |||||
| // 每个测试函数独立覆盖一个领域边界,首个异常即终止测试进程 | |||||
| testRegisterAddressBoundaries(); | |||||
| testRegisterAddressParsing(); | |||||
| testRegisterRepositorySeparatesAreas(); | |||||
| testMultiWordCodecsAndBlockAccess(); | |||||
| testHmiControlRegistryCompleteness(); | |||||
| testStatusTextDomainRules(); | |||||
| testMultiWordHmiBoundaries(); | |||||
| testHmiAppearancePropertyBoundaries(); | |||||
| testButtonEnableConditionBoundaries(); | |||||
| testLogicNodeConfigurationBoundaries(); | |||||
| testEdgeAndCommentBoundaries(); | |||||
| testDataInstructionBoundaries(); | |||||
| testLadderLogicBoundaries(); | |||||
| testModelsValidateBindingsAndIdentifiers(); | |||||
| testMultiPageAndLogicDomainRules(); | |||||
| testQuantityBoundaries(); | |||||
| testRuntimeConfiguredProjectLimits(); | |||||
| testRuntimeStateBoundaries(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "domain tests failed: " << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "domain tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("domain tests", { | |||||
| {"testRegisterAddressBoundaries", testRegisterAddressBoundaries}, | |||||
| {"testRegisterAddressParsing", testRegisterAddressParsing}, | |||||
| {"testRegisterRepositorySeparatesAreas", testRegisterRepositorySeparatesAreas}, | |||||
| {"testMultiWordCodecsAndBlockAccess", testMultiWordCodecsAndBlockAccess}, | |||||
| {"testHmiControlRegistryCompleteness", testHmiControlRegistryCompleteness}, | |||||
| {"testStatusTextDomainRules", testStatusTextDomainRules}, | |||||
| {"testMultiWordHmiBoundaries", testMultiWordHmiBoundaries}, | |||||
| {"testHmiAppearancePropertyBoundaries", testHmiAppearancePropertyBoundaries}, | |||||
| {"testButtonEnableConditionBoundaries", testButtonEnableConditionBoundaries}, | |||||
| {"testLogicNodeConfigurationBoundaries", testLogicNodeConfigurationBoundaries}, | |||||
| {"testEdgeAndCommentBoundaries", testEdgeAndCommentBoundaries}, | |||||
| {"testDataInstructionBoundaries", testDataInstructionBoundaries}, | |||||
| {"testLadderLogicBoundaries", testLadderLogicBoundaries}, | |||||
| {"testModelsValidateBindingsAndIdentifiers", testModelsValidateBindingsAndIdentifiers}, | |||||
| {"testMultiPageAndLogicDomainRules", testMultiPageAndLogicDomainRules}, | |||||
| {"testQuantityBoundaries", testQuantityBoundaries}, | |||||
| {"testRuntimeConfiguredProjectLimits", testRuntimeConfiguredProjectLimits}, | |||||
| {"testRuntimeStateBoundaries", testRuntimeStateBoundaries}, | |||||
| }); | |||||
| } | } | ||||
| @@ -290,6 +290,12 @@ void testRuntimeUsesRegisterRepository() | |||||
| const HmiRuntimeReadResult indicator_value = runtime_service.readControl(indicator); | const HmiRuntimeReadResult indicator_value = runtime_service.readControl(indicator); | ||||
| require(indicator_value.succeeded && indicator_value.bit_value, | require(indicator_value.succeeded && indicator_value.bit_value, | ||||
| "indicators must read M values through the repository"); | "indicators must read M values through the repository"); | ||||
| } | |||||
| void testNumericControlsUseRegisterRepository() | |||||
| { | |||||
| VirtualRegisterRepository repository; | |||||
| HmiRuntimeService runtime_service(repository); | |||||
| HmiControl numeric_input; | HmiControl numeric_input; | ||||
| numeric_input.id = "target"; | numeric_input.id = "target"; | ||||
| @@ -325,30 +331,20 @@ void testRuntimeUsesRegisterRepository() | |||||
| int32_input.binding = RegisterAddress{RegisterArea::D, 30}; | int32_input.binding = RegisterAddress{RegisterArea::D, 30}; | ||||
| require(runtime_service.writeNumericInput(int32_input, 305419896).succeeded, | require(runtime_service.writeNumericInput(int32_input, 305419896).succeeded, | ||||
| "Int32 numeric input must write two consecutive D words"); | "Int32 numeric input must write two consecutive D words"); | ||||
| const WordsReadResult int32_words = repository.readWords(*int32_input.binding, 2); | |||||
| const HmiRuntimeReadResult int32_value = runtime_service.readControl(int32_input); | const HmiRuntimeReadResult int32_value = runtime_service.readControl(int32_input); | ||||
| require(int32_words.succeeded | |||||
| && static_cast<std::uint16_t>(int32_words.values[0]) == 0x5678U | |||||
| && static_cast<std::uint16_t>(int32_words.values[1]) == 0x1234U | |||||
| && int32_value.succeeded | |||||
| require(int32_value.succeeded | |||||
| && std::get<std::int32_t>(int32_value.numeric_value) == 0x12345678, | && std::get<std::int32_t>(int32_value.numeric_value) == 0x12345678, | ||||
| "Int32 HMI read/write must use low-word-first Xinje ordering"); | |||||
| "Int32 HMI read/write must preserve the typed value"); | |||||
| HmiControl double_input = numeric_input; | HmiControl double_input = numeric_input; | ||||
| double_input.dataType = RegisterDataType::Float64; | double_input.dataType = RegisterDataType::Float64; | ||||
| double_input.binding = RegisterAddress{RegisterArea::D, 40}; | double_input.binding = RegisterAddress{RegisterArea::D, 40}; | ||||
| require(runtime_service.writeNumericInput(double_input, 1.0).succeeded, | require(runtime_service.writeNumericInput(double_input, 1.0).succeeded, | ||||
| "Double numeric input must write four consecutive D words"); | "Double numeric input must write four consecutive D words"); | ||||
| const WordsReadResult double_words = repository.readWords(*double_input.binding, 4); | |||||
| const HmiRuntimeReadResult double_value = runtime_service.readControl(double_input); | const HmiRuntimeReadResult double_value = runtime_service.readControl(double_input); | ||||
| require(double_words.succeeded | |||||
| && static_cast<std::uint16_t>(double_words.values[0]) == 0x0000U | |||||
| && static_cast<std::uint16_t>(double_words.values[1]) == 0x0000U | |||||
| && static_cast<std::uint16_t>(double_words.values[2]) == 0x0000U | |||||
| && static_cast<std::uint16_t>(double_words.values[3]) == 0x3ff0U | |||||
| && double_value.succeeded | |||||
| require(double_value.succeeded | |||||
| && std::get<double>(double_value.numeric_value) == 1.0, | && std::get<double>(double_value.numeric_value) == 1.0, | ||||
| "Double HMI read/write must use four low-address-first words"); | |||||
| "Double HMI read/write must preserve the typed value"); | |||||
| require(!runtime_service.writeNumericInput( | require(!runtime_service.writeNumericInput( | ||||
| double_input, std::numeric_limits<double>::quiet_NaN()).succeeded, | double_input, std::numeric_limits<double>::quiet_NaN()).succeeded, | ||||
| "Double numeric input must reject NaN"); | "Double numeric input must reject NaN"); | ||||
| @@ -356,7 +352,6 @@ void testRuntimeUsesRegisterRepository() | |||||
| require(runtime_service.writeNumericInput(double_input, 1.0).error | require(runtime_service.writeNumericInput(double_input, 1.0).error | ||||
| == HmiRuntimeError::InvalidBinding, | == HmiRuntimeError::InvalidBinding, | ||||
| "Double numeric input must reject odd or overflowing start addresses"); | "Double numeric input must reject odd or overflowing start addresses"); | ||||
| } | } | ||||
| void testStatusTextRuntimeMapping() | void testStatusTextRuntimeMapping() | ||||
| @@ -756,27 +751,18 @@ void testConfiguredPageDefaultsAndLimits() | |||||
| int main() | int main() | ||||
| { | { | ||||
| try | |||||
| { | |||||
| // 编辑和运行场景分别验证服务层两条独立职责 | |||||
| testControlEditing(); | |||||
| testBatchControlAlignment(); | |||||
| testHistoryAndAtomicBatchDelete(); | |||||
| testBatchPasteControls(); | |||||
| testRuntimeValidationAndPasteBoundaries(); | |||||
| testAppearanceEditing(); | |||||
| testRuntimeUsesRegisterRepository(); | |||||
| testStatusTextRuntimeMapping(); | |||||
| testPageLifecycleAndNavigation(); | |||||
| testPageResizeIsAtomicAndUndoable(); | |||||
| testConfiguredPageDefaultsAndLimits(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "HMI editor service tests failed: " << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "HMI editor service tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("HMI editor service tests", { | |||||
| {"testControlEditing", testControlEditing}, | |||||
| {"testBatchControlAlignment", testBatchControlAlignment}, | |||||
| {"testHistoryAndAtomicBatchDelete", testHistoryAndAtomicBatchDelete}, | |||||
| {"testBatchPasteControls", testBatchPasteControls}, | |||||
| {"testRuntimeValidationAndPasteBoundaries", testRuntimeValidationAndPasteBoundaries}, | |||||
| {"testAppearanceEditing", testAppearanceEditing}, | |||||
| {"testRuntimeUsesRegisterRepository", testRuntimeUsesRegisterRepository}, | |||||
| {"testNumericControlsUseRegisterRepository", testNumericControlsUseRegisterRepository}, | |||||
| {"testStatusTextRuntimeMapping", testStatusTextRuntimeMapping}, | |||||
| {"testPageLifecycleAndNavigation", testPageLifecycleAndNavigation}, | |||||
| {"testPageResizeIsAtomicAndUndoable", testPageResizeIsAtomicAndUndoable}, | |||||
| {"testConfiguredPageDefaultsAndLimits", testConfiguredPageDefaultsAndLimits}, | |||||
| }); | |||||
| } | } | ||||
| @@ -1488,42 +1488,33 @@ void testDoubleCoilCheckRemainsIndependent() | |||||
| int main() | int main() | ||||
| { | { | ||||
| try | |||||
| { | |||||
| testContinuousGridAndIndependentHorizontalWires(); | |||||
| testIndependentVerticalConnectionsAndNetworkSplit(); | |||||
| testNetworkCommentsFollowNetworkHeadsAndMergeAtomically(); | |||||
| testInsertRowSplitsVerticalEdges(); | |||||
| testDeleteRowMergesOnlyContinuousEdges(); | |||||
| testParallelBranchCreatesConnectedVisualRow(); | |||||
| testParallelBranchReusesExistingEdges(); | |||||
| testNodeDeletionAndHistoryAreAtomic(); | |||||
| testSelectionDeletionIsAtomic(); | |||||
| testInvalidSelectionDeletionDoesNotMutateOrRecordHistory(); | |||||
| testCommandInputMapsToGridCoordinates(); | |||||
| testProjectRungLimitAppliesToBranchAndPaste(); | |||||
| testConfiguredRowLimit(); | |||||
| testFirstEditOnEmptyLogicIsAtomic(); | |||||
| testCursorAdvanceAndOutputTransaction(); | |||||
| testOutputAutomaticallyCompletesTrailingWires(); | |||||
| testOutputAdvancesPastTheWholeNetworkGroup(); | |||||
| testOutputLimitFailureLeavesNoPartialEdit(); | |||||
| testSingleWireClipboardPasteAndUndo(); | |||||
| testMixedAndSparseGridClipboardFragments(); | |||||
| testGridClipboardFailuresAreAtomic(); | |||||
| testOutputAndVerticalClipboardRules(); | |||||
| testWholeRowClipboardInsertionAndLimit(); | |||||
| testSyntaxCheckNormalizesUnusedWiresAsOneEdit(); | |||||
| testSyntaxCheckPreservesValidParallelPath(); | |||||
| testSyntaxCheckKeepsBrokenOutputForErrorLocation(); | |||||
| testDoubleCoilCheckRemainsIndependent(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "logic editor service tests failed: " | |||||
| << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "logic editor service tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("logic editor service tests", { | |||||
| {"testContinuousGridAndIndependentHorizontalWires", testContinuousGridAndIndependentHorizontalWires}, | |||||
| {"testIndependentVerticalConnectionsAndNetworkSplit", testIndependentVerticalConnectionsAndNetworkSplit}, | |||||
| {"testNetworkCommentsFollowNetworkHeadsAndMergeAtomically", testNetworkCommentsFollowNetworkHeadsAndMergeAtomically}, | |||||
| {"testInsertRowSplitsVerticalEdges", testInsertRowSplitsVerticalEdges}, | |||||
| {"testDeleteRowMergesOnlyContinuousEdges", testDeleteRowMergesOnlyContinuousEdges}, | |||||
| {"testParallelBranchCreatesConnectedVisualRow", testParallelBranchCreatesConnectedVisualRow}, | |||||
| {"testParallelBranchReusesExistingEdges", testParallelBranchReusesExistingEdges}, | |||||
| {"testNodeDeletionAndHistoryAreAtomic", testNodeDeletionAndHistoryAreAtomic}, | |||||
| {"testSelectionDeletionIsAtomic", testSelectionDeletionIsAtomic}, | |||||
| {"testInvalidSelectionDeletionDoesNotMutateOrRecordHistory", testInvalidSelectionDeletionDoesNotMutateOrRecordHistory}, | |||||
| {"testCommandInputMapsToGridCoordinates", testCommandInputMapsToGridCoordinates}, | |||||
| {"testProjectRungLimitAppliesToBranchAndPaste", testProjectRungLimitAppliesToBranchAndPaste}, | |||||
| {"testConfiguredRowLimit", testConfiguredRowLimit}, | |||||
| {"testFirstEditOnEmptyLogicIsAtomic", testFirstEditOnEmptyLogicIsAtomic}, | |||||
| {"testCursorAdvanceAndOutputTransaction", testCursorAdvanceAndOutputTransaction}, | |||||
| {"testOutputAutomaticallyCompletesTrailingWires", testOutputAutomaticallyCompletesTrailingWires}, | |||||
| {"testOutputAdvancesPastTheWholeNetworkGroup", testOutputAdvancesPastTheWholeNetworkGroup}, | |||||
| {"testOutputLimitFailureLeavesNoPartialEdit", testOutputLimitFailureLeavesNoPartialEdit}, | |||||
| {"testSingleWireClipboardPasteAndUndo", testSingleWireClipboardPasteAndUndo}, | |||||
| {"testMixedAndSparseGridClipboardFragments", testMixedAndSparseGridClipboardFragments}, | |||||
| {"testGridClipboardFailuresAreAtomic", testGridClipboardFailuresAreAtomic}, | |||||
| {"testOutputAndVerticalClipboardRules", testOutputAndVerticalClipboardRules}, | |||||
| {"testWholeRowClipboardInsertionAndLimit", testWholeRowClipboardInsertionAndLimit}, | |||||
| {"testSyntaxCheckNormalizesUnusedWiresAsOneEdit", testSyntaxCheckNormalizesUnusedWiresAsOneEdit}, | |||||
| {"testSyntaxCheckPreservesValidParallelPath", testSyntaxCheckPreservesValidParallelPath}, | |||||
| {"testSyntaxCheckKeepsBrokenOutputForErrorLocation", testSyntaxCheckKeepsBrokenOutputForErrorLocation}, | |||||
| {"testDoubleCoilCheckRemainsIndependent", testDoubleCoilCheckRemainsIndependent}, | |||||
| }); | |||||
| } | } | ||||
| @@ -901,33 +901,25 @@ void testOnlineMonitorPreservesFaultAfterStopping() | |||||
| int main(int argc, char *argv[]) | int main(int argc, char *argv[]) | ||||
| { | { | ||||
| QCoreApplication application(argc, argv); | QCoreApplication application(argc, argv); | ||||
| try | |||||
| { | |||||
| testSeriesParallelContactsAndSequentialVisibility(); | |||||
| testParallelRowsAndColumnPropagation(); | |||||
| testUnconditionalCoil(); | |||||
| testEnabledEmptyRowIsRejectedBeforeScanning(); | |||||
| testWirePassThroughAndPowerTrace(); | |||||
| testMotorForwardReverseSelfHoldAndInterlockTruthTable(); | |||||
| testAllComparisons(); | |||||
| testSetResetAndDisabledLogic(); | |||||
| testMultipleLogicScanOrderAndTraceIsolation(); | |||||
| testEdgeContactsAreOneScanPulsesAndAreLogicScoped(); | |||||
| testMoveAndSaturatingArithmetic(); | |||||
| testSetResetPairOnSameAddress(); | |||||
| testConflictingCoilsAreRejected(); | |||||
| testHmiSimulationClosedLoop(); | |||||
| testSimulationLifecycleSnapshotAndFault(); | |||||
| testSimulationUsesInitialValuesAndDiscardsRuntimeOutputs(); | |||||
| testRepositoryFailureEntersFaultState(); | |||||
| testOnlineMonitorUsesTemporaryRegistersWithoutWritingPlcSource(); | |||||
| testOnlineMonitorPreservesFaultAfterStopping(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "offline simulation service tests failed: " << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "offline simulation service tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("offline simulation service tests", { | |||||
| {"testSeriesParallelContactsAndSequentialVisibility", testSeriesParallelContactsAndSequentialVisibility}, | |||||
| {"testParallelRowsAndColumnPropagation", testParallelRowsAndColumnPropagation}, | |||||
| {"testUnconditionalCoil", testUnconditionalCoil}, | |||||
| {"testEnabledEmptyRowIsRejectedBeforeScanning", testEnabledEmptyRowIsRejectedBeforeScanning}, | |||||
| {"testWirePassThroughAndPowerTrace", testWirePassThroughAndPowerTrace}, | |||||
| {"testMotorForwardReverseSelfHoldAndInterlockTruthTable", testMotorForwardReverseSelfHoldAndInterlockTruthTable}, | |||||
| {"testAllComparisons", testAllComparisons}, | |||||
| {"testSetResetAndDisabledLogic", testSetResetAndDisabledLogic}, | |||||
| {"testMultipleLogicScanOrderAndTraceIsolation", testMultipleLogicScanOrderAndTraceIsolation}, | |||||
| {"testEdgeContactsAreOneScanPulsesAndAreLogicScoped", testEdgeContactsAreOneScanPulsesAndAreLogicScoped}, | |||||
| {"testMoveAndSaturatingArithmetic", testMoveAndSaturatingArithmetic}, | |||||
| {"testSetResetPairOnSameAddress", testSetResetPairOnSameAddress}, | |||||
| {"testConflictingCoilsAreRejected", testConflictingCoilsAreRejected}, | |||||
| {"testHmiSimulationClosedLoop", testHmiSimulationClosedLoop}, | |||||
| {"testSimulationLifecycleSnapshotAndFault", testSimulationLifecycleSnapshotAndFault}, | |||||
| {"testSimulationUsesInitialValuesAndDiscardsRuntimeOutputs", testSimulationUsesInitialValuesAndDiscardsRuntimeOutputs}, | |||||
| {"testRepositoryFailureEntersFaultState", testRepositoryFailureEntersFaultState}, | |||||
| {"testOnlineMonitorUsesTemporaryRegistersWithoutWritingPlcSource", testOnlineMonitorUsesTemporaryRegistersWithoutWritingPlcSource}, | |||||
| {"testOnlineMonitorPreservesFaultAfterStopping", testOnlineMonitorPreservesFaultAfterStopping}, | |||||
| }); | |||||
| } | } | ||||
| @@ -6,6 +6,7 @@ | |||||
| #include <QDebug> | #include <QDebug> | ||||
| #include <QTest> | #include <QTest> | ||||
| #include <cstdint> | |||||
| #include <string> | #include <string> | ||||
| #include <utility> | #include <utility> | ||||
| #include <vector> | #include <vector> | ||||
| @@ -31,29 +32,38 @@ LogicNode coil(const std::string &id, int address) | |||||
| true}; | true}; | ||||
| } | } | ||||
| ControlLogic makeLogic(int index) | |||||
| ControlLogic makeLogic(int index, int rung_count = 1) | |||||
| { | { | ||||
| LadderRung rung; | |||||
| rung.id = "rung-" + std::to_string(index); | |||||
| rung.name = rung.id; | |||||
| for (int column = 0; | |||||
| column < ProjectLimits::kMaximumConditionColumns; | |||||
| ++column) | |||||
| ControlLogic logic; | |||||
| logic.id = "logic-" + std::to_string(index); | |||||
| logic.name = logic.id; | |||||
| logic.enabled = true; | |||||
| for (int rung_index = 0; rung_index < rung_count; ++rung_index) | |||||
| { | { | ||||
| rung.cells.push_back({ | |||||
| rung.id + "-cell-" + std::to_string(column), | |||||
| column == 0 ? LadderCellKind::Node : LadderCellKind::Wire, | |||||
| column == 0 | |||||
| ? std::optional<LogicNode>{contact( | |||||
| "contact-" + std::to_string(index), index)} | |||||
| : std::nullopt}); | |||||
| const int object_index = index * rung_count + rung_index; | |||||
| LadderRung rung; | |||||
| rung.id = "rung-" + std::to_string(index) + "-" | |||||
| + std::to_string(rung_index); | |||||
| rung.name = rung.id; | |||||
| for (int column = 0; | |||||
| column < ProjectLimits::kMaximumConditionColumns; | |||||
| ++column) | |||||
| { | |||||
| rung.cells.push_back({ | |||||
| rung.id + "-cell-" + std::to_string(column), | |||||
| column == 0 ? LadderCellKind::Node : LadderCellKind::Wire, | |||||
| column == 0 | |||||
| ? std::optional<LogicNode>{contact( | |||||
| "contact-" + std::to_string(object_index), | |||||
| object_index)} | |||||
| : std::nullopt}); | |||||
| } | |||||
| rung.output = coil( | |||||
| "coil-" + std::to_string(object_index), object_index + 1000); | |||||
| logic.rungs.push_back(std::move(rung)); | |||||
| } | } | ||||
| rung.output = coil("coil-" + std::to_string(index), index + 100); | |||||
| return {"logic-" + std::to_string(index), | |||||
| "logic-" + std::to_string(index), | |||||
| {rung}, | |||||
| true, | |||||
| {}}; | |||||
| return logic; | |||||
| } | } | ||||
| class PerformanceTests final : public QObject | class PerformanceTests final : public QObject | ||||
| @@ -64,14 +74,42 @@ private slots: | |||||
| void initTestCase() | void initTestCase() | ||||
| { | { | ||||
| qInfo().noquote() | qInfo().noquote() | ||||
| << QStringLiteral("性能测试开始:下面两项 RESULT 的耗时均为平均每次执行时间"); | |||||
| << QStringLiteral("性能测试开始:以下 RESULT 的耗时均为平均每次执行时间"); | |||||
| qInfo().noquote() | |||||
| << QStringLiteral("测试项目 1:工程上限规模的软件扫描"); | |||||
| qInfo().noquote() | |||||
| << QStringLiteral("测试项目 2:超出工程上限的扫描压力测试"); | |||||
| qInfo().noquote() | qInfo().noquote() | ||||
| << QStringLiteral("测试项目 1:100 个控制逻辑的软件扫描"); | |||||
| << QStringLiteral("测试项目 3:4001 个虚拟 M 地址的边界读写"); | |||||
| qInfo().noquote() | qInfo().noquote() | ||||
| << QStringLiteral("测试项目 2:M0 到 M4000 共 4001 个虚拟 M 地址的完整写入和读取"); | |||||
| << QStringLiteral("测试项目 4:256 个连续 D 字的轮询规模读写"); | |||||
| } | |||||
| void softwareExecutor_scansMaximumValidProject() | |||||
| { | |||||
| std::vector<ControlLogic> logics; | |||||
| logics.reserve(ProjectLimits::kMaximumControlLogics); | |||||
| for (std::size_t index = 0; | |||||
| index < ProjectLimits::kMaximumControlLogics; | |||||
| ++index) | |||||
| { | |||||
| logics.push_back(makeLogic( | |||||
| static_cast<int>(index), 64)); | |||||
| } | |||||
| SoftwareLogicExecutor executor; | |||||
| VirtualRegisterRepository repository; | |||||
| QVERIFY(executor.validate(logics).succeeded); | |||||
| QVERIFY(executor.executeScan(logics, repository).succeeded); | |||||
| QBENCHMARK | |||||
| { | |||||
| executor.executeScan(logics, repository); | |||||
| } | |||||
| QVERIFY(executor.executeScan(logics, repository).succeeded); | |||||
| } | } | ||||
| void softwareExecutor_scansRepresentativeProject() | |||||
| void softwareExecutor_scansStressProject() | |||||
| { | { | ||||
| std::vector<ControlLogic> logics; | std::vector<ControlLogic> logics; | ||||
| logics.reserve(100); | logics.reserve(100); | ||||
| @@ -83,15 +121,15 @@ private slots: | |||||
| SoftwareLogicExecutor executor; | SoftwareLogicExecutor executor; | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| QVERIFY(executor.validate(logics).succeeded); | QVERIFY(executor.validate(logics).succeeded); | ||||
| QVERIFY(executor.executeScan(logics, repository).succeeded); | |||||
| QBENCHMARK | QBENCHMARK | ||||
| { | { | ||||
| executor.executeScan(logics, repository); | executor.executeScan(logics, repository); | ||||
| } | } | ||||
| QVERIFY(executor.executeScan(logics, repository).succeeded); | |||||
| } | } | ||||
| void virtualRepository_handlesBoundaryWorkload() | |||||
| void virtualRepository_handlesBoundaryBitWorkload() | |||||
| { | { | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| for (int index = 0; index <= RegisterAddress::kMaximumIndex; ++index) | for (int index = 0; index <= RegisterAddress::kMaximumIndex; ++index) | ||||
| @@ -111,12 +149,43 @@ private slots: | |||||
| repository.readBit({RegisterArea::M, index}); | repository.readBit({RegisterArea::M, index}); | ||||
| } | } | ||||
| } | } | ||||
| QVERIFY(repository.readBit({RegisterArea::M, 0}).succeeded); | |||||
| QVERIFY(repository.readBit({RegisterArea::M, RegisterAddress::kMaximumIndex}).succeeded); | |||||
| } | |||||
| void virtualRepository_handlesPollSizedWordBlocks() | |||||
| { | |||||
| const int word_count = static_cast<int>(ProjectLimits::kMaximumPollAddresses); | |||||
| std::vector<std::int16_t> values(static_cast<std::size_t>(word_count)); | |||||
| for (int index = 0; index < word_count; ++index) | |||||
| { | |||||
| values[static_cast<std::size_t>(index)] = | |||||
| static_cast<std::int16_t>(index); | |||||
| } | |||||
| VirtualRegisterRepository repository; | |||||
| QVERIFY(repository.writeWords( | |||||
| {RegisterArea::D, 0}, values) | |||||
| .succeeded); | |||||
| QVERIFY(repository.readWords( | |||||
| {RegisterArea::D, 0}, word_count) | |||||
| .succeeded); | |||||
| QBENCHMARK | |||||
| { | |||||
| repository.writeWords({RegisterArea::D, 0}, values); | |||||
| repository.readWords({RegisterArea::D, 0}, word_count); | |||||
| } | |||||
| const WordsReadResult result = repository.readWords( | |||||
| {RegisterArea::D, 0}, word_count); | |||||
| QVERIFY(result.succeeded && result.values == values); | |||||
| } | } | ||||
| void cleanupTestCase() | void cleanupTestCase() | ||||
| { | { | ||||
| qInfo().noquote() | qInfo().noquote() | ||||
| << QStringLiteral("性能测试结束:请结合上方两项 RESULT 和最后的 Totals 判断结果"); | |||||
| << QStringLiteral("性能测试结束:请结合上方四项 RESULT 和最后的 Totals 判断结果"); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -186,18 +186,9 @@ int main(int argc, char *argv[]) | |||||
| { | { | ||||
| qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen")); | qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen")); | ||||
| QApplication application(argc, argv); | QApplication application(argc, argv); | ||||
| try | |||||
| { | |||||
| testDiscoveryFindsAndAcceptsConfiguration(); | |||||
| testDiscoveryCanBeCancelled(); | |||||
| testPortRefreshRemovesUnavailableSelection(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "PLC connection dialog tests failed: " | |||||
| << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "PLC connection dialog tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("PLC connection dialog tests", { | |||||
| {"testDiscoveryFindsAndAcceptsConfiguration", testDiscoveryFindsAndAcceptsConfiguration}, | |||||
| {"testDiscoveryCanBeCancelled", testDiscoveryCanBeCancelled}, | |||||
| {"testPortRefreshRemovesUnavailableSelection", testPortRefreshRemovesUnavailableSelection}, | |||||
| }); | |||||
| } | } | ||||
| @@ -529,21 +529,13 @@ void testPlcPollQuantityBoundaries() | |||||
| int main() | int main() | ||||
| { | { | ||||
| try | |||||
| { | |||||
| testPlcCacheAndWriteForwarding(); | |||||
| testRuntimeRepositorySwitchingAndDisconnect(); | |||||
| testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect(); | |||||
| testPlcCommunicationErrorClassification(); | |||||
| testPlcConfigurationBoundaries(); | |||||
| testPlcDiscoveryCandidatePriorityAndCoverage(); | |||||
| testPlcPollQuantityBoundaries(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "PLC runtime tests failed: " << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "PLC runtime tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("PLC runtime tests", { | |||||
| {"testPlcCacheAndWriteForwarding", testPlcCacheAndWriteForwarding}, | |||||
| {"testRuntimeRepositorySwitchingAndDisconnect", testRuntimeRepositorySwitchingAndDisconnect}, | |||||
| {"testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect", testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect}, | |||||
| {"testPlcCommunicationErrorClassification", testPlcCommunicationErrorClassification}, | |||||
| {"testPlcConfigurationBoundaries", testPlcConfigurationBoundaries}, | |||||
| {"testPlcDiscoveryCandidatePriorityAndCoverage", testPlcDiscoveryCandidatePriorityAndCoverage}, | |||||
| {"testPlcPollQuantityBoundaries", testPlcPollQuantityBoundaries}, | |||||
| }); | |||||
| } | } | ||||
| @@ -593,25 +593,16 @@ void testRegisterCommentService() | |||||
| int main() | int main() | ||||
| { | { | ||||
| try | |||||
| { | |||||
| testEmptyProjectRoundTrip(); | |||||
| testGridProjectRoundTrip(); | |||||
| testMOffAlarmRoundTrip(); | |||||
| testMultiWordHmiDataTypeRoundTrip(); | |||||
| testStatusTextRoundTrip(); | |||||
| testButtonEnableConditionRoundTrip(); | |||||
| testStrictVersionAndRequiredFields(); | |||||
| testInvalidGridAndConnectionsAreRejected(); | |||||
| testProjectServiceStateAndConfiguredLimits(); | |||||
| testRegisterCommentService(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "project management tests failed: " | |||||
| << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "project management tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("project management tests", { | |||||
| {"testEmptyProjectRoundTrip", testEmptyProjectRoundTrip}, | |||||
| {"testGridProjectRoundTrip", testGridProjectRoundTrip}, | |||||
| {"testMOffAlarmRoundTrip", testMOffAlarmRoundTrip}, | |||||
| {"testMultiWordHmiDataTypeRoundTrip", testMultiWordHmiDataTypeRoundTrip}, | |||||
| {"testStatusTextRoundTrip", testStatusTextRoundTrip}, | |||||
| {"testButtonEnableConditionRoundTrip", testButtonEnableConditionRoundTrip}, | |||||
| {"testStrictVersionAndRequiredFields", testStrictVersionAndRequiredFields}, | |||||
| {"testInvalidGridAndConnectionsAreRejected", testInvalidGridAndConnectionsAreRejected}, | |||||
| {"testProjectServiceStateAndConfiguredLimits", testProjectServiceStateAndConfiguredLimits}, | |||||
| {"testRegisterCommentService", testRegisterCommentService}, | |||||
| }); | |||||
| } | } | ||||
| @@ -274,7 +274,11 @@ void testMultiWordMonitoring() | |||||
| require(std::get<std::int32_t>(values[3].numericValue) == 0x12345678 | require(std::get<std::int32_t>(values[3].numericValue) == 0x12345678 | ||||
| && std::get<double>(values[5].numericValue) == 1.25, | && std::get<double>(values[5].numericValue) == 1.25, | ||||
| "Int32 and Double monitor values must decode to their exact types"); | "Int32 and Double monitor values must decode to their exact types"); | ||||
| } | |||||
| void testMultiWordMonitoringBoundaries() | |||||
| { | |||||
| VirtualRegisterRepository repository; | |||||
| RegisterMonitorService boundary_service(repository); | RegisterMonitorService boundary_service(repository); | ||||
| require(boundary_service.addRange( | require(boundary_service.addRange( | ||||
| "D3999", 1, RegisterDataType::Int32).succeeded | "D3999", 1, RegisterDataType::Int32).succeeded | ||||
| @@ -309,20 +313,13 @@ void testMultiWordMonitoring() | |||||
| int main() | int main() | ||||
| { | { | ||||
| try | |||||
| { | |||||
| testRangeManagement(); | |||||
| testSharedActiveRepositoryValues(); | |||||
| testEditingPlcMonitorAccess(); | |||||
| testRegisterWrites(); | |||||
| testOfflineInitialValueCapture(); | |||||
| testMultiWordMonitoring(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "register monitor service tests failed: " << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "register monitor service tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("register monitor service tests", { | |||||
| {"testRangeManagement", testRangeManagement}, | |||||
| {"testSharedActiveRepositoryValues", testSharedActiveRepositoryValues}, | |||||
| {"testEditingPlcMonitorAccess", testEditingPlcMonitorAccess}, | |||||
| {"testRegisterWrites", testRegisterWrites}, | |||||
| {"testOfflineInitialValueCapture", testOfflineInitialValueCapture}, | |||||
| {"testMultiWordMonitoring", testMultiWordMonitoring}, | |||||
| {"testMultiWordMonitoringBoundaries", testMultiWordMonitoringBoundaries}, | |||||
| }); | |||||
| } | } | ||||
| @@ -481,19 +481,9 @@ void testDisconnectedOutputBlocksOfflineAndOnlineRuntime() | |||||
| int main() | int main() | ||||
| { | { | ||||
| try | |||||
| { | |||||
| // 运行模式只有这一组状态机边界测试 | |||||
| testModeTransitions(); | |||||
| testMonitorPollRangeRollback(); | |||||
| testDisconnectedOutputBlocksOfflineAndOnlineRuntime(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "runtime mode service tests failed: " << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "runtime mode service tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("runtime mode service tests", { | |||||
| {"testModeTransitions", testModeTransitions}, | |||||
| {"testMonitorPollRangeRollback", testMonitorPollRangeRollback}, | |||||
| {"testDisconnectedOutputBlocksOfflineAndOnlineRuntime", testDisconnectedOutputBlocksOfflineAndOnlineRuntime}, | |||||
| }); | |||||
| } | } | ||||
| @@ -1387,29 +1387,19 @@ int main(int argc, char *argv[]) | |||||
| { | { | ||||
| qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen")); | qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen")); | ||||
| QApplication application(argc, argv); | QApplication application(argc, argv); | ||||
| try | |||||
| { | |||||
| testAlarmConfigurationOffersMOnAndMOff(); | |||||
| testMonitorOffersAllNumericTypes(); | |||||
| testAlarmListKeepsFixedGeometryWhileRecordsChange(); | |||||
| testHmiPropertyPanelInfersFixedBindingAreas(); | |||||
| testQueuedOfflineTraceIsIgnoredAfterReturningToEditing(); | |||||
| testLogicEditorGridSelectionAndDeletion(); | |||||
| testLadderLayoutAndDragDeletion(); | |||||
| testWireGesturePreviewKeepsAtomicCommit(); | |||||
| testEscapeExitsMouseWireMode(); | |||||
| testCursorAdvanceAndInlineCommandInput(); | |||||
| testVerticalWireShortcutAdvancesDownward(); | |||||
| testSegmentLevelTraceProjection(); | |||||
| testLogicClipboardUsesExplicitObjectAndRowSelection(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "runtime panel controller tests failed: " | |||||
| << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "runtime panel controller tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("runtime panel controller tests", { | |||||
| {"testAlarmConfigurationOffersMOnAndMOff", testAlarmConfigurationOffersMOnAndMOff}, | |||||
| {"testMonitorOffersAllNumericTypes", testMonitorOffersAllNumericTypes}, | |||||
| {"testAlarmListKeepsFixedGeometryWhileRecordsChange", testAlarmListKeepsFixedGeometryWhileRecordsChange}, | |||||
| {"testHmiPropertyPanelInfersFixedBindingAreas", testHmiPropertyPanelInfersFixedBindingAreas}, | |||||
| {"testQueuedOfflineTraceIsIgnoredAfterReturningToEditing", testQueuedOfflineTraceIsIgnoredAfterReturningToEditing}, | |||||
| {"testLogicEditorGridSelectionAndDeletion", testLogicEditorGridSelectionAndDeletion}, | |||||
| {"testLadderLayoutAndDragDeletion", testLadderLayoutAndDragDeletion}, | |||||
| {"testWireGesturePreviewKeepsAtomicCommit", testWireGesturePreviewKeepsAtomicCommit}, | |||||
| {"testEscapeExitsMouseWireMode", testEscapeExitsMouseWireMode}, | |||||
| {"testCursorAdvanceAndInlineCommandInput", testCursorAdvanceAndInlineCommandInput}, | |||||
| {"testVerticalWireShortcutAdvancesDownward", testVerticalWireShortcutAdvancesDownward}, | |||||
| {"testSegmentLevelTraceProjection", testSegmentLevelTraceProjection}, | |||||
| {"testLogicClipboardUsesExplicitObjectAndRowSelection", testLogicClipboardUsesExplicitObjectAndRowSelection}, | |||||
| }); | |||||
| } | } | ||||
| @@ -68,17 +68,7 @@ void testBundleRoundTripAndValidation() | |||||
| int main() | int main() | ||||
| { | { | ||||
| try | |||||
| { | |||||
| testBundleRoundTripAndValidation(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "runtime project bundle tests failed: " | |||||
| << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "runtime project bundle tests passed\n"; | |||||
| return 0; | |||||
| return TestSupport::runTestSuite("runtime project bundle tests", { | |||||
| {"testBundleRoundTripAndValidation", testBundleRoundTripAndValidation}, | |||||
| }); | |||||
| } | } | ||||
| @@ -2,6 +2,9 @@ | |||||
| #include "domain/project_storage.h" | #include "domain/project_storage.h" | ||||
| #include <cstddef> | |||||
| #include <initializer_list> | |||||
| #include <iostream> | |||||
| #include <stdexcept> | #include <stdexcept> | ||||
| #include <string> | #include <string> | ||||
| @@ -16,6 +19,45 @@ inline void require(bool condition, const std::string &message) | |||||
| } | } | ||||
| } | } | ||||
| struct TestCase | |||||
| { | |||||
| const char *name; | |||||
| void (*run)(); | |||||
| }; | |||||
| // 每个用例独立记录结果,避免首个失败掩盖同一目标中的其他回归 | |||||
| inline int runTestSuite( | |||||
| const char *suite_name, | |||||
| std::initializer_list<TestCase> test_cases) | |||||
| { | |||||
| std::size_t failed_count = 0U; | |||||
| for (const TestCase &test_case : test_cases) | |||||
| { | |||||
| try | |||||
| { | |||||
| test_case.run(); | |||||
| std::cout << "[PASS] " << test_case.name << '\n'; | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| ++failed_count; | |||||
| std::cerr << "[FAIL] " << test_case.name << ": " | |||||
| << error.what() << '\n'; | |||||
| } | |||||
| catch (...) | |||||
| { | |||||
| ++failed_count; | |||||
| std::cerr << "[FAIL] " << test_case.name | |||||
| << ": unknown exception\n"; | |||||
| } | |||||
| } | |||||
| const std::size_t passed_count = test_cases.size() - failed_count; | |||||
| std::cout << suite_name << ": " << passed_count << "/" | |||||
| << test_cases.size() << " test cases passed\n"; | |||||
| return failed_count == 0U ? 0 : 1; | |||||
| } | |||||
| // 编辑服务测试只验证内存模型和服务契约,不应依赖真实文件系统 | // 编辑服务测试只验证内存模型和服务契约,不应依赖真实文件系统 | ||||
| class InMemoryProjectStorage final : public ProjectStorage | class InMemoryProjectStorage final : public ProjectStorage | ||||
| { | { | ||||