|
- #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 <iostream>
- #include <cmath>
- #include <limits>
- #include <stdexcept>
- #include <string>
- #include <variant>
-
- 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<MonitorValue> values = service.values(false);
- require(values.size() == 2U && values[0].bitValue
- && std::get<std::int16_t>(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<std::int16_t>(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);
- 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<MonitorValue> values = service.values(false);
- require(values.size() == 2U
- && values[0].state == MonitorValueState::Unavailable
- && values[1].state == MonitorValueState::Unavailable
- && !service.writeBit({RegisterArea::M, 0}, false).succeeded,
- "editing PLC monitor must wait for the first read before access");
-
- service.setEditingPlcState(true, true);
- values = service.values(false);
- require(values[0].state == MonitorValueState::Valid && values[0].bitValue
- && values[1].state == MonitorValueState::Valid
- && std::get<std::int16_t>(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.setEditingPlcState(false, false);
- values = service.values(false);
- require(!values[0].bitValue
- && std::get<std::int16_t>(values[1].numericValue) == 11,
- "disconnecting PLC must restore the virtual editing source");
- }
-
- 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<std::int16_t>(-32768)).succeeded
- && repository.readWord(d0).value == -32768
- && service.writeNumeric(
- d0, RegisterDataType::Int16, static_cast<std::int16_t>(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<std::uint16_t>(captured_int32.values[0]) == 0x5678U
- && static_cast<std::uint16_t>(captured_int32.values[1]) == 0x1234U
- && captured_double.succeeded
- && static_cast<std::uint16_t>(captured_double.values[0]) == 0x0000U
- && static_cast<std::uint16_t>(captured_double.values[1]) == 0x0000U
- && static_cast<std::uint16_t>(captured_double.values[2]) == 0x0000U
- && static_cast<std::uint16_t>(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>{
- 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<MonitorValue> values = service.values(false);
- require(values.front().state == MonitorValueState::Valid
- && std::fabs(
- std::get<float>(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<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
- && !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<RegisterAddress> &,
- const std::vector<RegisterWordRange> &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},
- });
- }
|