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;
///
/// Register row 54: the real per-site data-connection gauges. Before these existed the only
/// connection-ish series was scadabridge_site_connection_up, 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.
///
///
/// 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 QueueDepthGaugeTests pass alone and fail under the full
/// concurrent suite. Each test also removes its own connections in a finally so it
/// leaves the shared registry exactly as it found it.
///
public sealed class DataConnectionGaugeTests
{
private const string ConnectedGauge = "scadabridge.site.data_connections.connected";
private const string TotalGauge = "scadabridge.site.data_connections.total";
/// Verifies that the connected state raises both the connected and total gauges.
[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);
}
}
/// Verifies that a non-connected state raises only the total gauge.
/// The state.
[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);
}
}
/// Verifies that a transition from connected to disconnected drops the connected gauge but keeps the total.
[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);
}
}
/// Verifies that removing a data connection drops it from both gauges.
[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));
}
/// Verifies that removing a data connection is idempotent.
[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);
}
/// Verifies that a null or empty connection name is ignored.
[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));
}
/// Verifies that the stream subscriber gauge is published under its renamed name.
[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}";
/// Forces one observation of the named gauge and returns its value.
/// Instrument name to observe.
/// The observed value, or 0 if the instrument never reported.
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((_, measurement, _, _) => observed = measurement);
listener.Start();
listener.RecordObservableInstruments();
return observed;
}
/// Collects every instrument name published on the ScadaBridge meter.
/// The published instrument names.
private static IReadOnlyCollection PublishedInstrumentNames()
{
var names = new List();
using var listener = new MeterListener();
listener.InstrumentPublished = (instrument, _) =>
{
if (instrument.Meter.Name == ScadaBridgeTelemetry.MeterName)
{
names.Add(instrument.Name);
}
};
listener.Start();
return names;
}
}