Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientDiagnosticsTests.cs
T

132 lines
5.1 KiB
C#

using System.Diagnostics.Metrics;
using Opc.Ua;
using Opc.Ua.Client;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
/// <summary>
/// #276 — session/publish events are published on the <c>otopcua.driver.opcuaclient.events</c> OTel counter.
/// The listener filters on a UNIQUE <c>driver_instance</c> tag: the Meter is process-static, so a
/// name-only filter would also collect parallel tests' measurements.
/// </summary>
[Trait("Category", "Unit")]
public sealed class OpcUaClientDiagnosticsTests
{
private sealed class Capture : IDisposable
{
private readonly MeterListener _listener = new();
private readonly object _gate = new();
private readonly Dictionary<string, long> _byEvent = new();
public Capture(string instance)
{
_listener.InstrumentPublished = (inst, l) =>
{
if (inst.Name == "otopcua.driver.opcuaclient.events") l.EnableMeasurementEvents(inst);
};
_listener.SetMeasurementEventCallback<long>((_, 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.ReconnectAttempt);
d.Record(OpcUaClientDiagnostics.ReconnectSuccess);
d.Record(OpcUaClientDiagnostics.ReconnectExhausted);
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.ReconnectAttempt].ShouldBe(1);
cap[OpcUaClientDiagnostics.ReconnectSuccess].ShouldBe(1);
cap[OpcUaClientDiagnostics.ReconnectExhausted].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);
}
/// <summary>
/// #275: the driver does not split Read/Write/CreateMonitoredItems itself because the SDK session already
/// does — <c>Session</c> derives from <c>SessionClientBatched</c> (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.
/// </summary>
[Fact]
public void The_sdk_session_is_the_batching_session_type()
{
typeof(SessionClientBatched).IsAssignableFrom(typeof(Session)).ShouldBeTrue();
}
}