#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 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 && values[1].wordValue == -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 && values[1].wordValue == 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 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.writeWord(d0, -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.writeWord(m0, 123); require(!wrong_word.succeeded && wrong_word.error == RegisterError::AreaMismatch, "monitor word writes must reject M addresses"); require(service.writeWord(d0, static_cast(-32768)).succeeded && repository.readWord(d0).value == -32768 && service.writeWord(d0, 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.writeWord({RegisterArea::D, 10}, -456).succeeded && service.writeFloat({RegisterArea::D, 20}, 1.5f).succeeded, "editing monitor writes must succeed against the virtual repository"); require(initial_repository.readBit({RegisterArea::M, 3}).value && initial_repository.readWord({RegisterArea::D, 10}).value == -456 && std::fabs(Float32Codec::decode( initial_repository.readWordPair({RegisterArea::D, 20}).values[0], initial_repository.readWordPair({RegisterArea::D, 20}).values[1]) .value_or(0.0f) - 1.5f) < 0.000001f, "editing monitor writes must be captured as offline initial values"); 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 testFloat32Monitoring() { 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.float32Starts().size() == 3U && 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.writeFloat({RegisterArea::D, 1}, -2.5f).succeeded, "Float32 monitor writes must use the pair write path"); const std::vector values = service.values(false); require(values.front().state == MonitorValueState::Valid && std::fabs(values.front().floatValue - (-2.5f)) < 0.000001f, "Float32 monitor values must decode the pair correctly"); require(!service.writeFloat({RegisterArea::D, 4000}, 1.0f).succeeded, "Float32 monitor writes at D4000 must be rejected"); } } // namespace int main() { try { testRangeManagement(); testSharedActiveRepositoryValues(); testRegisterWrites(); testOfflineInitialValueCapture(); testFloat32Monitoring(); } 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; }