513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
199 lines
8.2 KiB
C#
199 lines
8.2 KiB
C#
using System.Diagnostics.Metrics;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Observability;
|
|
|
|
/// <summary>
|
|
/// Register row 54: the real per-site data-connection gauges. Before these existed the only
|
|
/// connection-ish series was <c>scadabridge_site_connection_up</c>, which counted open
|
|
/// central→site gRPC subscriber streams and sat at 0 on a healthy unwatched site — so there
|
|
/// was no metric that answered "are this site's devices actually connected?" at all.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Every assertion here is a DELTA against a baseline taken inside the test, and every
|
|
/// connection name is unique to the test. That is deliberate: the registry behind these
|
|
/// gauges is a process-global static, so absolute assertions would inherit the register
|
|
/// row 32 flake that makes <c>QueueDepthGaugeTests</c> pass alone and fail under the full
|
|
/// concurrent suite. Each test also removes its own connections in a <c>finally</c> so it
|
|
/// leaves the shared registry exactly as it found it.
|
|
/// </remarks>
|
|
public sealed class DataConnectionGaugeTests
|
|
{
|
|
private const string ConnectedGauge = "scadabridge.site.data_connections.connected";
|
|
private const string TotalGauge = "scadabridge.site.data_connections.total";
|
|
|
|
/// <summary>Verifies that the connected state raises both the connected and total gauges.</summary>
|
|
[Fact]
|
|
public void ConnectedState_RaisesConnectedAndTotal()
|
|
{
|
|
var name = UniqueName();
|
|
var connectedBefore = ReadGauge(ConnectedGauge);
|
|
var totalBefore = ReadGauge(TotalGauge);
|
|
|
|
try
|
|
{
|
|
ScadaBridgeTelemetry.RecordDataConnectionState(name, ConnectionHealth.Connected);
|
|
|
|
Assert.Equal(connectedBefore + 1, ReadGauge(ConnectedGauge));
|
|
Assert.Equal(totalBefore + 1, ReadGauge(TotalGauge));
|
|
}
|
|
finally
|
|
{
|
|
ScadaBridgeTelemetry.RemoveDataConnection(name);
|
|
}
|
|
}
|
|
|
|
/// <summary>Verifies that a non-connected state raises only the total gauge.</summary>
|
|
/// <param name="state">The state.</param>
|
|
[Theory]
|
|
[InlineData(ConnectionHealth.Disconnected)]
|
|
[InlineData(ConnectionHealth.Connecting)]
|
|
[InlineData(ConnectionHealth.Error)]
|
|
public void NonConnectedState_RaisesTotalOnly(ConnectionHealth state)
|
|
{
|
|
// The load-bearing shape of the pair: a device that is configured but not talking
|
|
// must show up in the denominator and NOT in the numerator, because connected < total
|
|
// is the whole alert condition. A gauge that only counted connected rows would read
|
|
// identically for "one device down" and "device deleted".
|
|
var name = UniqueName();
|
|
var connectedBefore = ReadGauge(ConnectedGauge);
|
|
var totalBefore = ReadGauge(TotalGauge);
|
|
|
|
try
|
|
{
|
|
ScadaBridgeTelemetry.RecordDataConnectionState(name, state);
|
|
|
|
Assert.Equal(connectedBefore, ReadGauge(ConnectedGauge));
|
|
Assert.Equal(totalBefore + 1, ReadGauge(TotalGauge));
|
|
}
|
|
finally
|
|
{
|
|
ScadaBridgeTelemetry.RemoveDataConnection(name);
|
|
}
|
|
}
|
|
|
|
/// <summary>Verifies that a transition from connected to disconnected drops the connected gauge but keeps the total.</summary>
|
|
[Fact]
|
|
public void TransitionFromConnectedToDisconnected_DropsConnectedButKeepsTotal()
|
|
{
|
|
// The DCL calls Record* once per state transition on the SAME name, so a transition
|
|
// must overwrite rather than accumulate — otherwise a flapping connection would
|
|
// inflate the total without bound.
|
|
var name = UniqueName();
|
|
var connectedBefore = ReadGauge(ConnectedGauge);
|
|
var totalBefore = ReadGauge(TotalGauge);
|
|
|
|
try
|
|
{
|
|
ScadaBridgeTelemetry.RecordDataConnectionState(name, ConnectionHealth.Connected);
|
|
ScadaBridgeTelemetry.RecordDataConnectionState(name, ConnectionHealth.Disconnected);
|
|
|
|
Assert.Equal(connectedBefore, ReadGauge(ConnectedGauge));
|
|
Assert.Equal(totalBefore + 1, ReadGauge(TotalGauge));
|
|
}
|
|
finally
|
|
{
|
|
ScadaBridgeTelemetry.RemoveDataConnection(name);
|
|
}
|
|
}
|
|
|
|
/// <summary>Verifies that removing a data connection drops it from both gauges.</summary>
|
|
[Fact]
|
|
public void RemoveDataConnection_DropsItFromBothGauges()
|
|
{
|
|
// An undeployed connection must leave the denominator. If it lingered as a
|
|
// permanently not-connected row, connected < total would latch on forever and the
|
|
// alert would be useless after the first undeploy.
|
|
var name = UniqueName();
|
|
var connectedBefore = ReadGauge(ConnectedGauge);
|
|
var totalBefore = ReadGauge(TotalGauge);
|
|
|
|
ScadaBridgeTelemetry.RecordDataConnectionState(name, ConnectionHealth.Connected);
|
|
ScadaBridgeTelemetry.RemoveDataConnection(name);
|
|
|
|
Assert.Equal(connectedBefore, ReadGauge(ConnectedGauge));
|
|
Assert.Equal(totalBefore, ReadGauge(TotalGauge));
|
|
}
|
|
|
|
/// <summary>Verifies that removing a data connection is idempotent.</summary>
|
|
[Fact]
|
|
public void RemoveDataConnection_IsIdempotent()
|
|
{
|
|
var name = UniqueName();
|
|
ScadaBridgeTelemetry.RecordDataConnectionState(name, ConnectionHealth.Connected);
|
|
ScadaBridgeTelemetry.RemoveDataConnection(name);
|
|
|
|
var ex = Record.Exception(() => ScadaBridgeTelemetry.RemoveDataConnection(name));
|
|
|
|
Assert.Null(ex);
|
|
}
|
|
|
|
/// <summary>Verifies that a null or empty connection name is ignored.</summary>
|
|
[Fact]
|
|
public void NullOrEmptyConnectionName_IsIgnored()
|
|
{
|
|
// Guards the gauges against a malformed caller creating a phantom row that could
|
|
// never be removed by name.
|
|
var totalBefore = ReadGauge(TotalGauge);
|
|
|
|
ScadaBridgeTelemetry.RecordDataConnectionState(string.Empty, ConnectionHealth.Connected);
|
|
ScadaBridgeTelemetry.RecordDataConnectionState(null!, ConnectionHealth.Connected);
|
|
|
|
Assert.Equal(totalBefore, ReadGauge(TotalGauge));
|
|
}
|
|
|
|
/// <summary>Verifies that the stream subscriber gauge is published under its renamed name.</summary>
|
|
[Fact]
|
|
public void StreamSubscriberGauge_IsPublishedUnderTheRenamedName()
|
|
{
|
|
// Register row 54's rename half. Pins the honest name so a future edit cannot
|
|
// silently restore scadabridge.site.connection.up, whose meaning (open gRPC
|
|
// subscriber streams) never matched what operators read it as.
|
|
var names = PublishedInstrumentNames();
|
|
|
|
Assert.Contains("scadabridge.site.stream.subscribers.active", names);
|
|
Assert.DoesNotContain("scadabridge.site.connection.up", names);
|
|
}
|
|
|
|
private static string UniqueName() => $"conn-{Guid.NewGuid():N}";
|
|
|
|
/// <summary>Forces one observation of the named gauge and returns its value.</summary>
|
|
/// <param name="instrumentName">Instrument name to observe.</param>
|
|
/// <returns>The observed value, or 0 if the instrument never reported.</returns>
|
|
private static long ReadGauge(string instrumentName)
|
|
{
|
|
long observed = 0;
|
|
using var listener = new MeterListener();
|
|
listener.InstrumentPublished = (instrument, l) =>
|
|
{
|
|
if (instrument.Meter.Name == ScadaBridgeTelemetry.MeterName
|
|
&& instrument.Name == instrumentName)
|
|
{
|
|
l.EnableMeasurementEvents(instrument);
|
|
}
|
|
};
|
|
listener.SetMeasurementEventCallback<long>((_, measurement, _, _) => observed = measurement);
|
|
listener.Start();
|
|
listener.RecordObservableInstruments();
|
|
return observed;
|
|
}
|
|
|
|
/// <summary>Collects every instrument name published on the ScadaBridge meter.</summary>
|
|
/// <returns>The published instrument names.</returns>
|
|
private static IReadOnlyCollection<string> PublishedInstrumentNames()
|
|
{
|
|
var names = new List<string>();
|
|
using var listener = new MeterListener();
|
|
listener.InstrumentPublished = (instrument, _) =>
|
|
{
|
|
if (instrument.Meter.Name == ScadaBridgeTelemetry.MeterName)
|
|
{
|
|
names.Add(instrument.Name);
|
|
}
|
|
};
|
|
listener.Start();
|
|
return names;
|
|
}
|
|
}
|