using System; using ArchestrA.MxAccess; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Worker.MxAccess; using ComMxDataType = ArchestrA.MxAccess.MxDataType; namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess; /// /// Integrated tests for : drive an MXAccess COM /// event through the real sink → → /// pipeline and assert a correctly-converted /// protobuf lands in the queue. /// /// /// Boundary: the COM-side += subscription performed in /// casts the supplied object to the /// sealed LMXProxyServerClass RCW and cannot run without a live MXAccess COM /// object, so Attach/Detach are not exercised here. The event /// handlers themselves (OnDataChange, OnWriteComplete, /// OperationComplete, OnBufferedDataChange) are the exact delegate /// targets the COM runtime invokes; calling them directly reproduces an STA-thread /// COM callback and exercises the genuine conversion + enqueue path. The /// sessionId normally set by Attach defaults to empty here, which the /// assertions account for. The COM-event-conversion fault branch is left to /// and the queue's own fault tests. /// public sealed class MxAccessBaseEventSinkTests { /// /// Verifies that an OnDataChange COM callback converts to a protobuf event and lands in the queue. /// [Fact] public void OnDataChange_ComCallback_ConvertedEventLandsInQueue() { MxAccessEventQueue queue = new(); MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper()); DateTime timestamp = new(2026, 5, 18, 9, 15, 0, DateTimeKind.Utc); MXSTATUS_PROXY[] statuses = Array.Empty(); sink.OnDataChange( hLMXServerHandle: 7, phItemHandle: 21, pvItemValue: 1234, pwItemQuality: 192, pftItemTimeStamp: timestamp, ref statuses); Assert.Equal(1, queue.Count); Assert.Equal(1UL, queue.LastEventSequence); Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent)); Assert.NotNull(workerEvent); MxEvent mxEvent = workerEvent!.Event; Assert.Equal(MxEventFamily.OnDataChange, mxEvent.Family); Assert.Equal(MxEvent.BodyOneofCase.OnDataChange, mxEvent.BodyCase); Assert.Equal(7, mxEvent.ServerHandle); Assert.Equal(21, mxEvent.ItemHandle); Assert.Equal(1234, mxEvent.Value.Int32Value); Assert.Equal(192, mxEvent.Quality); Assert.Equal(timestamp, mxEvent.SourceTimestamp.ToDateTime()); Assert.Equal(1UL, mxEvent.WorkerSequence); Assert.NotNull(mxEvent.WorkerTimestamp); } /// /// Verifies that an OnDataChange COM callback also writes the value into the /// per-session value cache, so a later ReadBulk on an already-advised /// tag can serve the cached value without re-advising. The cache update must /// fire after the event has cleared the outbound queue — verified here by /// checking the cache only after the queue confirms the enqueue succeeded. /// [Fact] public void OnDataChange_ComCallback_PopulatesValueCache() { MxAccessEventQueue queue = new(); MxAccessValueCache cache = new(); MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper(), cache); DateTime timestamp = new(2026, 5, 18, 9, 15, 0, DateTimeKind.Utc); MXSTATUS_PROXY[] statuses = Array.Empty(); sink.OnDataChange( hLMXServerHandle: 7, phItemHandle: 21, pvItemValue: 1234, pwItemQuality: 192, pftItemTimeStamp: timestamp, ref statuses); Assert.Equal(1, queue.Count); Assert.True(cache.TryGet(7, 21, out MxAccessValueCache.CachedValue cached)); Assert.Equal(1UL, cached.Version); Assert.Equal(1234, cached.Value.Int32Value); Assert.Equal(192, cached.Quality); Assert.Equal(timestamp, cached.SourceTimestamp.ToDateTime()); } /// /// Verifies that the sink-bound ValueCache is exposed for sharing with /// the owning so writes and reads see the same /// instance. /// [Fact] public void ValueCache_ReturnsTheInstanceBoundAtConstruction() { MxAccessEventQueue queue = new(); MxAccessValueCache cache = new(); MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper(), cache); Assert.Same(cache, sink.ValueCache); } /// /// Verifies that consecutive OnDataChange callbacks land in the queue with monotonic sequences. /// [Fact] public void OnDataChange_MultipleComCallbacks_QueueAssignsMonotonicSequences() { MxAccessEventQueue queue = new(); MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper()); MXSTATUS_PROXY[] statuses = Array.Empty(); sink.OnDataChange(1, 10, 100, 192, DateTime.UtcNow, ref statuses); sink.OnDataChange(1, 11, 200, 192, DateTime.UtcNow, ref statuses); sink.OnDataChange(1, 12, 300, 192, DateTime.UtcNow, ref statuses); Assert.Equal(3, queue.Count); Assert.Equal(3UL, queue.LastEventSequence); Assert.True(queue.TryDequeue(out WorkerEvent? first)); Assert.True(queue.TryDequeue(out WorkerEvent? second)); Assert.True(queue.TryDequeue(out WorkerEvent? third)); Assert.Equal(1UL, first!.Event.WorkerSequence); Assert.Equal(2UL, second!.Event.WorkerSequence); Assert.Equal(3UL, third!.Event.WorkerSequence); Assert.Equal(10, first.Event.ItemHandle); Assert.Equal(12, third.Event.ItemHandle); } /// /// Verifies that an OnWriteComplete COM callback lands in the queue with the correct family. /// [Fact] public void OnWriteComplete_ComCallback_ConvertedEventLandsInQueue() { MxAccessEventQueue queue = new(); MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper()); MXSTATUS_PROXY[] statuses = Array.Empty(); sink.OnWriteComplete(hLMXServerHandle: 3, phItemHandle: 9, ref statuses); Assert.Equal(1, queue.Count); Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent)); MxEvent mxEvent = workerEvent!.Event; Assert.Equal(MxEventFamily.OnWriteComplete, mxEvent.Family); Assert.Equal(MxEvent.BodyOneofCase.OnWriteComplete, mxEvent.BodyCase); Assert.Equal(3, mxEvent.ServerHandle); Assert.Equal(9, mxEvent.ItemHandle); Assert.Equal(1UL, mxEvent.WorkerSequence); } /// /// Verifies that an OperationComplete COM callback lands in the queue with the correct family. /// [Fact] public void OperationComplete_ComCallback_ConvertedEventLandsInQueue() { MxAccessEventQueue queue = new(); MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper()); MXSTATUS_PROXY[] statuses = Array.Empty(); sink.OperationComplete(hLMXServerHandle: 4, phItemHandle: 8, ref statuses); Assert.Equal(1, queue.Count); Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent)); MxEvent mxEvent = workerEvent!.Event; Assert.Equal(MxEventFamily.OperationComplete, mxEvent.Family); Assert.Equal(MxEvent.BodyOneofCase.OperationComplete, mxEvent.BodyCase); Assert.Equal(4, mxEvent.ServerHandle); Assert.Equal(8, mxEvent.ItemHandle); } /// /// Verifies that an OnBufferedDataChange COM callback converts the value and lands in the queue. /// [Fact] public void OnBufferedDataChange_ComCallback_ConvertedEventLandsInQueue() { MxAccessEventQueue queue = new(); MxAccessBaseEventSink sink = new(queue, new MxAccessEventMapper()); MXSTATUS_PROXY[] statuses = Array.Empty(); // Raw MXAccess data-type code 2 == Integer (see MxAccessEventMapper.MapMxDataType). const int integerDataTypeCode = 2; sink.OnBufferedDataChange( hLMXServerHandle: 5, phItemHandle: 13, dtDataType: (ComMxDataType)integerDataTypeCode, pvItemValue: 77, pwItemQuality: 192, pftItemTimeStamp: DateTime.UtcNow, ref statuses); Assert.Equal(1, queue.Count); Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent)); MxEvent mxEvent = workerEvent!.Event; Assert.Equal(MxEventFamily.OnBufferedDataChange, mxEvent.Family); Assert.Equal(MxEvent.BodyOneofCase.OnBufferedDataChange, mxEvent.BodyCase); Assert.Equal(5, mxEvent.ServerHandle); Assert.Equal(13, mxEvent.ItemHandle); Assert.Equal(integerDataTypeCode, mxEvent.OnBufferedDataChange.RawDataType); } }