Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Observability/DataConnectionGaugeTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
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.
2026-10-07 15:05:49 -04:00

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;
}
}