#include "domain/active_register_repository.h" #include "domain/register_monitor_model.h" #include "domain/register_repository.h" #include "domain/virtual_register_repository.h" #include "services/register_monitor_service.h" #include "support/test_support.h" #include #include #include #include #include #include namespace { using TestSupport::require; void testRangeManagement() { VirtualRegisterRepository repository; RegisterMonitorService service(repository); int change_count = 0; service.setAddressesChangedCallback([&change_count] { ++change_count; }); const RegisterMonitorResult added = service.addRange(" d10 ", 3); require(added.succeeded && added.affectedCount == 3 && service.addresses().size() == 3U, "monitor service must expand a normalized continuous address range"); require(service.addresses().front() == RegisterAddress{RegisterArea::D, 10} && service.addresses().back() == RegisterAddress{RegisterArea::D, 12}, "monitor service must preserve expanded address order"); const RegisterMonitorResult duplicate = service.addRange("D11", 2); require(!duplicate.succeeded && duplicate.error == RegisterMonitorError::NoChange, "monitor service must reject a fully duplicated range"); require(!service.addRange("M4000", 2).succeeded, "monitor service must reject a range crossing address 4000"); require(change_count == 1, "failed additions must not report a monitor address change"); require(service.remove({RegisterAddress{RegisterArea::D, 11}}).succeeded && service.addresses().size() == 2U, "monitor service must remove selected addresses"); require(service.clear().succeeded && service.addresses().empty(), "monitor service must clear the current monitor table"); require(change_count == 3, "successful add, remove and clear operations must notify address changes"); } void testSharedActiveRepositoryValues() { VirtualRegisterRepository virtual_repository; VirtualRegisterRepository plc_repository; ActiveRegisterRepository active_repository(virtual_repository); RegisterMonitorService service(active_repository); require(service.addRange("M0", 1).succeeded && service.addRange("D0", 1).succeeded, "monitor service must accept one M and one D address"); virtual_repository.writeBit({RegisterArea::M, 0}, true); virtual_repository.writeWord({RegisterArea::D, 0}, -123); std::vector values = service.values(false); require(values.size() == 2U && values[0].bitValue && std::get(values[1].numericValue) == -123, "monitor values must reflect the active virtual repository"); plc_repository.writeBit({RegisterArea::M, 0}, false); plc_repository.writeWord({RegisterArea::D, 0}, 456); active_repository.use(plc_repository); values = service.values(false); require(!values[0].bitValue && std::get(values[1].numericValue) == 456, "monitor values must follow the active repository switch"); values = service.values(true); require(values[0].state == MonitorValueState::CommunicationFault && values[1].state == MonitorValueState::CommunicationFault, "valid cached values must be marked stale during a communication fault"); } void testEditingPlcMonitorAccess() { VirtualRegisterRepository virtual_repository; VirtualRegisterRepository plc_repository; ActiveRegisterRepository active_repository(virtual_repository); VirtualRegisterRepository initial_repository; RegisterMonitorService service(active_repository); service.setOfflineInitialRepository(initial_repository); service.setOfflineInitialCaptureEnabled(true); service.setEditingPlcRepository(plc_repository); service.setEditingModeActive(true); require(service.addRange("M0", 1).succeeded && service.addRange("D0", 1).succeeded, "editing PLC monitor fixture must accept M and D points"); virtual_repository.writeBit({RegisterArea::M, 0}, false); virtual_repository.writeWord({RegisterArea::D, 0}, 11); plc_repository.writeBit({RegisterArea::M, 0}, true); plc_repository.writeWord({RegisterArea::D, 0}, 22); service.setEditingPlcState(true, false); std::vector values = service.values(false); require(values.size() == 2U && !values[0].bitValue && std::get(values[1].numericValue) == 11, "connecting a PLC must not automatically replace the offline editing source"); const EditingMonitorSourceResult first_read_rejected = service.selectEditingSource(EditingMonitorSource::Plc); require(!first_read_rejected.succeeded && service.editingSource() == EditingMonitorSource::Offline, "editing monitor must reject PLC selection before the first read"); service.setEditingPlcState(true, true); require(service.editingSource() == EditingMonitorSource::Offline && service.selectEditingSource(EditingMonitorSource::Plc).succeeded, "a completed first read must allow an explicit PLC source selection"); values = service.values(false); require(values[0].state == MonitorValueState::Valid && values[0].bitValue && values[1].state == MonitorValueState::Valid && std::get(values[1].numericValue) == 22, "editing PLC monitor must display PLC cache values after first read"); require(service.writeBit({RegisterArea::M, 0}, false).succeeded && service.writeNumeric( {RegisterArea::D, 0}, RegisterDataType::Int16, 33).succeeded && !plc_repository.readBit({RegisterArea::M, 0}).value && plc_repository.readWord({RegisterArea::D, 0}).value == 33 && !initial_repository.readBit({RegisterArea::M, 0}).value && initial_repository.readWord({RegisterArea::D, 0}).value == 0, "editing PLC monitor writes must target PLC and skip offline capture"); service.setEditingModeActive(false); values = service.values(false); require(!values[0].bitValue && std::get(values[1].numericValue) == 11, "runtime access must keep following the active repository"); service.setEditingModeActive(true); service.setEditingPlcState(false, false); values = service.values(false); require(service.editingSource() == EditingMonitorSource::Plc && values[0].state == MonitorValueState::Unavailable && values[1].state == MonitorValueState::Unavailable && !service.writeBit({RegisterArea::M, 0}, true).succeeded, "a PLC disconnect must preserve the selected source and prohibit access"); service.setEditingPlcState(true, true); values = service.values(false); require(service.editingSource() == EditingMonitorSource::Plc && !values[0].bitValue && std::get(values[1].numericValue) == 33, "reconnection and a new first read must resume the selected PLC source"); service.setEditingPlcState(false, false); require(service.selectEditingSource(EditingMonitorSource::Offline).succeeded, "offline source must remain selectable while PLC is disconnected"); values = service.values(false); require(!values[0].bitValue && std::get(values[1].numericValue) == 11, "explicit offline selection must restore virtual editing values"); } void testRegisterWrites() { VirtualRegisterRepository repository; RegisterMonitorService service(repository); const RegisterAddress m0{RegisterArea::M, 0}; const RegisterAddress d0{RegisterArea::D, 0}; const RegisterMonitorWriteResult bit_write = service.writeBit(m0, true); require(bit_write.succeeded && repository.readBit(m0).value, "monitor service must write M values through the active repository"); const RegisterMonitorWriteResult word_write = service.writeNumeric( d0, RegisterDataType::Int16, -123); require(word_write.succeeded && repository.readWord(d0).value == -123, "monitor service must write signed D values through the active repository"); const RegisterMonitorWriteResult wrong_bit = service.writeBit(d0, true); require(!wrong_bit.succeeded && wrong_bit.error == RegisterError::AreaMismatch, "monitor bit writes must reject D addresses"); const RegisterMonitorWriteResult wrong_word = service.writeNumeric( m0, RegisterDataType::Int16, 123); require(!wrong_word.succeeded && wrong_word.error == RegisterError::AreaMismatch, "monitor word writes must reject M addresses"); require(service.writeNumeric( d0, RegisterDataType::Int16, static_cast(-32768)).succeeded && repository.readWord(d0).value == -32768 && service.writeNumeric( d0, RegisterDataType::Int16, static_cast(32767)).succeeded && repository.readWord(d0).value == 32767, "monitor word writes must preserve signed 16-bit boundary values"); } void testOfflineInitialValueCapture() { VirtualRegisterRepository virtual_repository; VirtualRegisterRepository initial_repository; ActiveRegisterRepository active_repository(virtual_repository); RegisterMonitorService service(active_repository); service.setOfflineInitialRepository(initial_repository); service.setOfflineInitialCaptureEnabled(true); require(service.writeBit({RegisterArea::M, 3}, true).succeeded && service.writeNumeric( {RegisterArea::D, 10}, RegisterDataType::Int16, -456).succeeded && service.writeNumeric( {RegisterArea::D, 20}, RegisterDataType::Float32, 1.5f).succeeded && service.writeNumeric( {RegisterArea::D, 30}, RegisterDataType::Int32, 305419896) .succeeded && service.writeNumeric( {RegisterArea::D, 40}, RegisterDataType::Float64, 1.0) .succeeded, "editing monitor writes must succeed against the virtual repository"); const WordsReadResult captured_int32 = initial_repository.readWords( {RegisterArea::D, 30}, 2); const WordsReadResult captured_double = initial_repository.readWords( {RegisterArea::D, 40}, 4); require(initial_repository.readBit({RegisterArea::M, 3}).value && initial_repository.readWord({RegisterArea::D, 10}).value == -456 && std::fabs(Float32Codec::decode( 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, "editing monitor writes must be captured as offline initial values"); require(captured_int32.succeeded && static_cast(captured_int32.values[0]) == 0x5678U && static_cast(captured_int32.values[1]) == 0x1234U && captured_double.succeeded && static_cast(captured_double.values[0]) == 0x0000U && static_cast(captured_double.values[1]) == 0x0000U && static_cast(captured_double.values[2]) == 0x0000U && static_cast(captured_double.values[3]) == 0x3ff0U, "offline initial capture must keep all Int32 and Double words"); service.setOfflineInitialCaptureEnabled(false); require(service.writeBit({RegisterArea::M, 3}, false).succeeded && initial_repository.readBit({RegisterArea::M, 3}).value, "runtime writes must not overwrite the captured initial values"); } void testMultiWordMonitoring() { VirtualRegisterRepository repository; RegisterMonitorService service(repository); const RegisterMonitorResult added = service.addRange( "D1", 3, RegisterDataType::Float32); require(added.succeeded && added.affectedCount == 3 && service.points().size() == 3U && service.points()[0].address == RegisterAddress{RegisterArea::D, 1} && service.points()[1].address == RegisterAddress{RegisterArea::D, 3} && service.points()[2].address == RegisterAddress{RegisterArea::D, 5}, "Float32 monitor batches must advance by two D words"); require(service.multiWordRanges().size() == 3U && service.multiWordRanges().front().wordCount == 2 && service.pollAddresses().size() == 6U, "Float32 monitor points must expose both words for polling"); const RegisterMonitorResult duplicate = service.addRange( "D1", 1, RegisterDataType::Float32); require(!duplicate.succeeded && duplicate.error == RegisterMonitorError::NoChange, "identical Float32 monitor points must be rejected"); const RegisterMonitorResult overlap = service.addRange( "D2", 1, RegisterDataType::Int16); require(overlap.succeeded && !overlap.message.empty(), "partially overlapping monitor points must be added with a warning"); require(service.remove(std::vector{ MonitorPoint{{RegisterArea::D, 2}, RegisterDataType::Int16}}).succeeded && service.points().size() == 3U, "overlapping monitor removal must identify the point type"); require(service.writeNumeric( {RegisterArea::D, 1}, RegisterDataType::Float32, -2.5f).succeeded, "Float32 monitor writes must use the pair write path"); std::vector values = service.values(false); require(values.front().state == MonitorValueState::Valid && std::fabs( std::get(values.front().numericValue) - (-2.5f)) < 0.000001f, "Float32 monitor values must decode the pair correctly"); require(!service.writeNumeric( {RegisterArea::D, 4000}, RegisterDataType::Float32, 1.0f).succeeded, "Float32 monitor writes at D4000 must be rejected"); const RegisterMonitorResult int32_added = service.addRange( "D10", 2, RegisterDataType::Int32); const RegisterMonitorResult double_added = service.addRange( "D20", 2, RegisterDataType::Float64); require(int32_added.succeeded && double_added.succeeded && service.points()[3].address == RegisterAddress{RegisterArea::D, 10} && service.points()[4].address == RegisterAddress{RegisterArea::D, 12} && service.points()[5].address == RegisterAddress{RegisterArea::D, 20} && service.points()[6].address == RegisterAddress{RegisterArea::D, 24} && service.pollAddresses().size() == 18U && service.multiWordRanges().size() == 7U && service.multiWordRanges()[5].wordCount == 4, "Int32 and Double monitor batches must advance by their word counts"); require(service.writeNumeric( {RegisterArea::D, 10}, RegisterDataType::Int32, 305419896) .succeeded && service.writeNumeric( {RegisterArea::D, 20}, RegisterDataType::Float64, 1.25) .succeeded, "Int32 and Double monitor writes must use continuous word blocks"); values = service.values(false); require(std::get(values[3].numericValue) == 0x12345678 && std::get(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 && !boundary_service.addRange( "D4000", 1, RegisterDataType::Int32).succeeded && boundary_service.addRange( "D3996", 1, RegisterDataType::Float64).succeeded && !boundary_service.addRange( "D3997", 1, RegisterDataType::Float64).succeeded && !boundary_service.addRange( "D21", 1, RegisterDataType::Float64).succeeded && !boundary_service.addRange( "M10", 1, RegisterDataType::Int32).succeeded, "multi-word monitor points must enforce D limits and Double alignment"); RegisterMonitorService validated_service(repository); validated_service.setPollConfigurationValidator( [](const std::vector &, const std::vector &ranges) { return ranges.empty() ? std::string{} : std::string{"轮询范围被拒绝"}; }); const RegisterMonitorResult rejected = validated_service.addRange( "D100", 1, RegisterDataType::Float64); require(!rejected.succeeded && rejected.error == RegisterMonitorError::PollConfigurationRejected && validated_service.points().empty(), "a rejected candidate poll configuration must not change monitor points"); } } // namespace int main() { return TestSupport::runTestSuite("register monitor service tests", { {"testRangeManagement", testRangeManagement}, {"testSharedActiveRepositoryValues", testSharedActiveRepositoryValues}, {"testEditingPlcMonitorAccess", testEditingPlcMonitorAccess}, {"testRegisterWrites", testRegisterWrites}, {"testOfflineInitialValueCapture", testOfflineInitialValueCapture}, {"testMultiWordMonitoring", testMultiWordMonitoring}, {"testMultiWordMonitoringBoundaries", testMultiWordMonitoringBoundaries}, }); }