using System; using System.Threading; using System.Threading.Tasks; using Shouldly; using Xunit; using ZB.MOM.WW.LmxOpcUa.Host.Configuration; using ZB.MOM.WW.LmxOpcUa.Host.Domain; using ZB.MOM.WW.LmxOpcUa.Host.Metrics; using ZB.MOM.WW.LmxOpcUa.Host.MxAccess; using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess { /// /// Verifies MXAccess client read and write behavior against the fake runtime proxy used by the bridge. /// public class MxAccessClientReadWriteTests : IDisposable { private readonly StaComThread _staThread; private readonly FakeMxProxy _proxy; private readonly PerformanceMetrics _metrics; private readonly MxAccessClient _client; /// /// Initializes the COM-threaded MXAccess test fixture with a fake runtime proxy and metrics collector. /// public MxAccessClientReadWriteTests() { _staThread = new StaComThread(); _staThread.Start(); _proxy = new FakeMxProxy(); _metrics = new PerformanceMetrics(); var config = new MxAccessConfiguration { ReadTimeoutSeconds = 2, WriteTimeoutSeconds = 2 }; _client = new MxAccessClient(_staThread, _proxy, config, _metrics); } /// /// Disposes the MXAccess client fixture and its supporting STA thread and metrics collector. /// public void Dispose() { _client.Dispose(); _staThread.Dispose(); _metrics.Dispose(); } /// /// Confirms that reads fail with bad-not-connected quality when the runtime session is offline. /// [Fact] public async Task Read_NotConnected_ReturnsBad() { var result = await _client.ReadAsync("Tag.Attr"); result.Quality.ShouldBe(Quality.BadNotConnected); } /// /// Confirms that a runtime data-change callback completes a pending read with the published value. /// [Fact] public async Task Read_ReturnsValueOnDataChange() { await _client.ConnectAsync(); // Start read in background var readTask = _client.ReadAsync("TestTag.Attr"); // Give it a moment to set up subscription, then simulate data change await Task.Delay(50); _proxy.SimulateDataChangeByAddress("TestTag.Attr", 42, 192); var result = await readTask; result.Value.ShouldBe(42); result.Quality.ShouldBe(Quality.Good); } /// /// Confirms that reads time out with bad communication-failure quality when the runtime never responds. /// [Fact] public async Task Read_Timeout_ReturnsBadCommFailure() { await _client.ConnectAsync(); // No data change simulated, so it will timeout var result = await _client.ReadAsync("TestTag.Attr"); result.Quality.ShouldBe(Quality.BadCommFailure); } /// /// Confirms that timed-out reads are recorded as failed read operations in the metrics collector. /// [Fact] public async Task Read_Timeout_RecordsFailedMetrics() { await _client.ConnectAsync(); var result = await _client.ReadAsync("TestTag.Attr"); result.Quality.ShouldBe(Quality.BadCommFailure); var stats = _metrics.GetStatistics(); stats.ShouldContainKey("Read"); stats["Read"].TotalCount.ShouldBe(1); stats["Read"].SuccessCount.ShouldBe(0); } /// /// Confirms that writes are rejected when the runtime session is not connected. /// [Fact] public async Task Write_NotConnected_ReturnsFalse() { var result = await _client.WriteAsync("Tag.Attr", 42); result.ShouldBe(false); } /// /// Confirms that successful runtime write acknowledgments return success and record the written payload. /// [Fact] public async Task Write_Success_ReturnsTrue() { await _client.ConnectAsync(); _proxy.WriteCompleteStatus = 0; var result = await _client.WriteAsync("TestTag.Attr", 42); result.ShouldBe(true); _proxy.WrittenValues.ShouldContain(w => w.Address == "TestTag.Attr" && (int)w.Value == 42); } /// /// Confirms that MXAccess error codes on write completion are surfaced as failed writes. /// [Fact] public async Task Write_ErrorCode_ReturnsFalse() { await _client.ConnectAsync(); _proxy.WriteCompleteStatus = 1012; // Wrong data type var result = await _client.WriteAsync("TestTag.Attr", "bad_value"); result.ShouldBe(false); } /// /// Confirms that write timeouts are recorded as failed write operations in the metrics collector. /// [Fact] public async Task Write_Timeout_ReturnsFalse_AndRecordsFailedMetrics() { await _client.ConnectAsync(); _proxy.SkipWriteCompleteCallback = true; var result = await _client.WriteAsync("TestTag.Attr", 42); result.ShouldBe(false); var stats = _metrics.GetStatistics(); stats.ShouldContainKey("Write"); stats["Write"].TotalCount.ShouldBe(1); stats["Write"].SuccessCount.ShouldBe(0); } /// /// Confirms that successful reads contribute a read entry to the metrics collector. /// [Fact] public async Task Read_RecordsMetrics() { await _client.ConnectAsync(); var readTask = _client.ReadAsync("TestTag.Attr"); await Task.Delay(50); _proxy.SimulateDataChangeByAddress("TestTag.Attr", 1, 192); await readTask; var stats = _metrics.GetStatistics(); stats.ShouldContainKey("Read"); stats["Read"].TotalCount.ShouldBe(1); } /// /// Confirms that writes contribute a write entry to the metrics collector. /// [Fact] public async Task Write_RecordsMetrics() { await _client.ConnectAsync(); await _client.WriteAsync("TestTag.Attr", 42); var stats = _metrics.GetStatistics(); stats.ShouldContainKey("Write"); stats["Write"].TotalCount.ShouldBe(1); } } }