using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests; /// #276 — counters are thread-safe and surface through DriverHealth.Diagnostics. [Trait("Category", "Unit")] public sealed class OpcUaClientDiagnosticsTests { [Fact] public void Fresh_snapshot_is_all_zero_with_the_full_key_set() { var snap = new OpcUaClientDiagnostics().Snapshot(); snap.Keys.ShouldBe( [ "PublishResponseCount", "NotificationCount", "DataChangeCount", "BadStatusValueCount", "PublishErrorCount", "KeepAliveMissCount", "ReconnectAttemptCount", "ReconnectSuccessCount", "ReconnectExhaustedCount", "LastReconnectUtcTicks", "ReadCallCount", "ReadFailureCount", "WriteCallCount", "WriteFailureCount", "ChunkedRequestCount", ], ignoreOrder: true); snap.Values.ShouldAllBe(v => v == 0); } [Fact] public void Each_recorder_moves_exactly_its_own_counter() { var d = new OpcUaClientDiagnostics(); d.RecordPublishResponse(); d.RecordNotifications(3); d.RecordDataChange(bad: false); d.RecordDataChange(bad: true); d.RecordPublishError(); d.RecordKeepAliveMiss(); d.RecordReconnectAttempt(); d.RecordReconnectSucceeded(); d.RecordReconnectExhausted(); d.RecordRead(failed: true); d.RecordWrite(failed: false); d.RecordChunkedRequest(); var s = d.Snapshot(); s["PublishResponseCount"].ShouldBe(1); s["NotificationCount"].ShouldBe(3); s["DataChangeCount"].ShouldBe(2); s["BadStatusValueCount"].ShouldBe(1); s["PublishErrorCount"].ShouldBe(1); s["KeepAliveMissCount"].ShouldBe(1); s["ReconnectAttemptCount"].ShouldBe(1); s["ReconnectSuccessCount"].ShouldBe(1); s["ReconnectExhaustedCount"].ShouldBe(1); s["LastReconnectUtcTicks"].ShouldBeGreaterThan(0); s["ReadCallCount"].ShouldBe(1); s["ReadFailureCount"].ShouldBe(1); s["WriteCallCount"].ShouldBe(1); s["WriteFailureCount"].ShouldBe(0); s["ChunkedRequestCount"].ShouldBe(1); } [Fact] public void Concurrent_increments_do_not_lose_updates() { var d = new OpcUaClientDiagnostics(); const int threads = 8, each = 5_000; Parallel.For(0, threads, _ => { for (var i = 0; i < each; i++) { d.RecordDataChange(bad: i % 2 == 0); d.RecordNotifications(2); } }); var s = d.Snapshot(); s["DataChangeCount"].ShouldBe(threads * each); s["BadStatusValueCount"].ShouldBe(threads * each / 2); s["NotificationCount"].ShouldBe(threads * each * 2); } [Fact] public void Snapshot_is_a_point_in_time_copy() { var d = new OpcUaClientDiagnostics(); var before = d.Snapshot(); d.RecordKeepAliveMiss(); before["KeepAliveMissCount"].ShouldBe(0); d.Snapshot()["KeepAliveMissCount"].ShouldBe(1); } [Fact] public void GetHealth_carries_the_diagnostics_and_keeps_state_fields() { using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-diag"); drv.DiagnosticsForTest.RecordKeepAliveMiss(); drv.DiagnosticsForTest.RecordKeepAliveMiss(); var h = drv.GetHealth(); h.State.ShouldBe(DriverState.Unknown); h.Diagnostics.ShouldNotBeNull(); h.Diagnostics!["KeepAliveMissCount"].ShouldBe(2); } [Fact] public void DriverHealth_without_diagnostics_stays_back_compatible() { var h = new DriverHealth(DriverState.Healthy, null, null); h.Diagnostics.ShouldBeNull(); h.ShouldBe(new DriverHealth(DriverState.Healthy, null, null)); } }