using System.Diagnostics.Metrics; using Opc.Ua; using Opc.Ua.Client; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests; /// /// #276 — session/publish events are published on the otopcua.driver.opcuaclient.events OTel counter. /// The listener filters on a UNIQUE driver_instance tag: the Meter is process-static, so a /// name-only filter would also collect parallel tests' measurements. /// [Trait("Category", "Unit")] public sealed class OpcUaClientDiagnosticsTests { internal sealed class Capture : IDisposable { private readonly MeterListener _listener = new(); private readonly object _gate = new(); private readonly Dictionary _byEvent = new(); public Capture(string instance) { _listener.InstrumentPublished = (inst, l) => { if (inst.Name == "otopcua.driver.opcuaclient.events") l.EnableMeasurementEvents(inst); }; _listener.SetMeasurementEventCallback((_, value, tags, _) => { string? driver = null, evt = null; foreach (var t in tags) { if (t.Key == "driver_instance") driver = t.Value as string; else if (t.Key == "event") evt = t.Value as string; } if (driver != instance || evt is null) return; lock (_gate) _byEvent[evt] = _byEvent.GetValueOrDefault(evt) + value; }); _listener.Start(); } public long this[string evt] { get { lock (_gate) return _byEvent.GetValueOrDefault(evt); } } public void Dispose() => _listener.Dispose(); } [Fact] public void Each_event_lands_on_its_own_tagged_series() { var id = $"diag-{Guid.NewGuid():N}"; using var cap = new Capture(id); var d = new OpcUaClientDiagnostics(id); d.Record(OpcUaClientDiagnostics.PublishResponse); d.Record(OpcUaClientDiagnostics.Notification, 3); d.Record(OpcUaClientDiagnostics.DataChange); d.Record(OpcUaClientDiagnostics.BadStatusValue); d.Record(OpcUaClientDiagnostics.PublishError); d.Record(OpcUaClientDiagnostics.KeepAliveMiss); d.Record(OpcUaClientDiagnostics.KeepAliveRecovered); cap[OpcUaClientDiagnostics.PublishResponse].ShouldBe(1); cap[OpcUaClientDiagnostics.Notification].ShouldBe(3); cap[OpcUaClientDiagnostics.DataChange].ShouldBe(1); cap[OpcUaClientDiagnostics.BadStatusValue].ShouldBe(1); cap[OpcUaClientDiagnostics.PublishError].ShouldBe(1); cap[OpcUaClientDiagnostics.KeepAliveMiss].ShouldBe(1); cap[OpcUaClientDiagnostics.KeepAliveRecovered].ShouldBe(1); } [Fact] public void Another_instances_events_are_not_attributed_to_this_one() { var mine = $"diag-{Guid.NewGuid():N}"; using var cap = new Capture(mine); // Positive control: my own event IS seen; the other instance's identical event is not. new OpcUaClientDiagnostics("someone-else").Record(OpcUaClientDiagnostics.KeepAliveMiss); new OpcUaClientDiagnostics(mine).Record(OpcUaClientDiagnostics.PublishError); cap[OpcUaClientDiagnostics.PublishError].ShouldBe(1); cap[OpcUaClientDiagnostics.KeepAliveMiss].ShouldBe(0); } [Fact] public void Concurrent_records_do_not_lose_updates() { var id = $"diag-{Guid.NewGuid():N}"; using var cap = new Capture(id); var d = new OpcUaClientDiagnostics(id); const int threads = 8, each = 2_000; Parallel.For(0, threads, _ => { for (var i = 0; i < each; i++) d.Record(OpcUaClientDiagnostics.DataChange); }); cap[OpcUaClientDiagnostics.DataChange].ShouldBe(threads * each); } [Fact] public void Zero_or_negative_counts_are_ignored() { var id = $"diag-{Guid.NewGuid():N}"; using var cap = new Capture(id); var d = new OpcUaClientDiagnostics(id); d.Record(OpcUaClientDiagnostics.Notification, 0); d.Record(OpcUaClientDiagnostics.Notification, -2); d.Record(OpcUaClientDiagnostics.DataChange); // positive control cap[OpcUaClientDiagnostics.Notification].ShouldBe(0); cap[OpcUaClientDiagnostics.DataChange].ShouldBe(1); } /// /// #275: the driver does not split Read/Write/CreateMonitoredItems itself because the SDK session already /// does — Session derives from SessionClientBatched (which batches by the server's /// OperationLimits, fetched on open). If an SDK bump ever drops that, this goes red and the driver needs /// its own chunking again. /// [Fact] public void The_sdk_session_is_the_batching_session_type() { typeof(SessionClientBatched).IsAssignableFrom(typeof(Session)).ShouldBeTrue(); } }