| @@ -140,17 +140,8 @@ void testConfiguredAlarmLimit() | |||
| 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[]) | |||
| { | |||
| 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() | |||
| { | |||
| 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); | |||
| require(indicator_value.succeeded && indicator_value.bit_value, | |||
| "indicators must read M values through the repository"); | |||
| } | |||
| void testNumericControlsUseRegisterRepository() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| HmiRuntimeService runtime_service(repository); | |||
| HmiControl numeric_input; | |||
| numeric_input.id = "target"; | |||
| @@ -325,30 +331,20 @@ void testRuntimeUsesRegisterRepository() | |||
| int32_input.binding = RegisterAddress{RegisterArea::D, 30}; | |||
| require(runtime_service.writeNumericInput(int32_input, 305419896).succeeded, | |||
| "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); | |||
| 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, | |||
| "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; | |||
| double_input.dataType = RegisterDataType::Float64; | |||
| double_input.binding = RegisterAddress{RegisterArea::D, 40}; | |||
| require(runtime_service.writeNumericInput(double_input, 1.0).succeeded, | |||
| "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); | |||
| 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, | |||
| "Double HMI read/write must use four low-address-first words"); | |||
| "Double HMI read/write must preserve the typed value"); | |||
| require(!runtime_service.writeNumericInput( | |||
| double_input, std::numeric_limits<double>::quiet_NaN()).succeeded, | |||
| "Double numeric input must reject NaN"); | |||
| @@ -356,7 +352,6 @@ void testRuntimeUsesRegisterRepository() | |||
| require(runtime_service.writeNumericInput(double_input, 1.0).error | |||
| == HmiRuntimeError::InvalidBinding, | |||
| "Double numeric input must reject odd or overflowing start addresses"); | |||
| } | |||
| void testStatusTextRuntimeMapping() | |||
| @@ -756,27 +751,18 @@ void testConfiguredPageDefaultsAndLimits() | |||
| 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() | |||
| { | |||
| 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[]) | |||
| { | |||
| 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 <QTest> | |||
| #include <cstdint> | |||
| #include <string> | |||
| #include <utility> | |||
| #include <vector> | |||
| @@ -31,29 +32,38 @@ LogicNode coil(const std::string &id, int address) | |||
| 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 | |||
| @@ -64,14 +74,42 @@ private slots: | |||
| void initTestCase() | |||
| { | |||
| qInfo().noquote() | |||
| << QStringLiteral("性能测试开始:下面两项 RESULT 的耗时均为平均每次执行时间"); | |||
| << QStringLiteral("性能测试开始:以下 RESULT 的耗时均为平均每次执行时间"); | |||
| qInfo().noquote() | |||
| << QStringLiteral("测试项目 1:工程上限规模的软件扫描"); | |||
| qInfo().noquote() | |||
| << QStringLiteral("测试项目 2:超出工程上限的扫描压力测试"); | |||
| qInfo().noquote() | |||
| << QStringLiteral("测试项目 1:100 个控制逻辑的软件扫描"); | |||
| << QStringLiteral("测试项目 3:4001 个虚拟 M 地址的边界读写"); | |||
| 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; | |||
| logics.reserve(100); | |||
| @@ -83,15 +121,15 @@ private slots: | |||
| 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 virtualRepository_handlesBoundaryWorkload() | |||
| void virtualRepository_handlesBoundaryBitWorkload() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| for (int index = 0; index <= RegisterAddress::kMaximumIndex; ++index) | |||
| @@ -111,12 +149,43 @@ private slots: | |||
| 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() | |||
| { | |||
| qInfo().noquote() | |||
| << QStringLiteral("性能测试结束:请结合上方两项 RESULT 和最后的 Totals 判断结果"); | |||
| << QStringLiteral("性能测试结束:请结合上方四项 RESULT 和最后的 Totals 判断结果"); | |||
| } | |||
| }; | |||
| @@ -186,18 +186,9 @@ int main(int argc, char *argv[]) | |||
| { | |||
| qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen")); | |||
| 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() | |||
| { | |||
| 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() | |||
| { | |||
| 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 | |||
| && std::get<double>(values[5].numericValue) == 1.25, | |||
| "Int32 and Double monitor values must decode to their exact types"); | |||
| } | |||
| void testMultiWordMonitoringBoundaries() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| RegisterMonitorService boundary_service(repository); | |||
| require(boundary_service.addRange( | |||
| "D3999", 1, RegisterDataType::Int32).succeeded | |||
| @@ -309,20 +313,13 @@ void testMultiWordMonitoring() | |||
| 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() | |||
| { | |||
| 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")); | |||
| 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() | |||
| { | |||
| 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 <cstddef> | |||
| #include <initializer_list> | |||
| #include <iostream> | |||
| #include <stdexcept> | |||
| #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 | |||
| { | |||