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.
326 lines
12 KiB
C#
326 lines
12 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
|
|
|
public class SiteHealthCollectorTests
|
|
{
|
|
private readonly SiteHealthCollector _collector = new();
|
|
|
|
/// <summary>Verifies that a report has zero counters when no errors have been recorded.</summary>
|
|
[Fact]
|
|
public void CollectReport_ReturnsZeroCounters_WhenNoErrorsRecorded()
|
|
{
|
|
var report = _collector.CollectReport("site-1");
|
|
|
|
Assert.Equal("site-1", report.SiteId);
|
|
Assert.Equal(0, report.ScriptErrorCount);
|
|
Assert.Equal(0, report.AlarmEvaluationErrorCount);
|
|
Assert.Equal(0, report.DeadLetterCount);
|
|
}
|
|
|
|
/// <summary>Verifies that script errors accumulate between reports.</summary>
|
|
[Fact]
|
|
public void IncrementScriptError_AccumulatesBetweenReports()
|
|
{
|
|
_collector.IncrementScriptError();
|
|
_collector.IncrementScriptError();
|
|
_collector.IncrementScriptError();
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
Assert.Equal(3, report.ScriptErrorCount);
|
|
}
|
|
|
|
/// <summary>Verifies that alarm errors accumulate between reports.</summary>
|
|
[Fact]
|
|
public void IncrementAlarmError_AccumulatesBetweenReports()
|
|
{
|
|
_collector.IncrementAlarmError();
|
|
_collector.IncrementAlarmError();
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
Assert.Equal(2, report.AlarmEvaluationErrorCount);
|
|
}
|
|
|
|
/// <summary>Verifies that a report carries the script scheduler statistics.</summary>
|
|
[Fact]
|
|
public void CollectReport_CarriesScriptSchedulerStats()
|
|
{
|
|
_collector.SetScriptSchedulerStats(queueDepth: 7, busyThreads: 3, oldestBusyAgeSeconds: 42.5);
|
|
var report = _collector.CollectReport("site-1");
|
|
Assert.Equal(7, report.ScriptQueueDepth);
|
|
Assert.Equal(3, report.ScriptBusyThreads);
|
|
Assert.Equal(42.5, report.ScriptOldestBusyAgeSeconds);
|
|
}
|
|
|
|
/// <summary>Verifies that script scheduler statistics default to zero and null.</summary>
|
|
[Fact]
|
|
public void CollectReport_ScriptSchedulerStats_DefaultToZeroAndNull()
|
|
{
|
|
var report = _collector.CollectReport("site-1");
|
|
Assert.Equal(0, report.ScriptQueueDepth);
|
|
Assert.Equal(0, report.ScriptBusyThreads);
|
|
Assert.Null(report.ScriptOldestBusyAgeSeconds);
|
|
}
|
|
|
|
/// <summary>Verifies that dead letters accumulate between reports.</summary>
|
|
[Fact]
|
|
public void IncrementDeadLetter_AccumulatesBetweenReports()
|
|
{
|
|
_collector.IncrementDeadLetter();
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
Assert.Equal(1, report.DeadLetterCount);
|
|
}
|
|
|
|
/// <summary>Verifies that collecting a report resets the counters.</summary>
|
|
[Fact]
|
|
public void CollectReport_ResetsCounters_AfterCollection()
|
|
{
|
|
_collector.IncrementScriptError();
|
|
_collector.IncrementAlarmError();
|
|
_collector.IncrementDeadLetter();
|
|
|
|
var first = _collector.CollectReport("site-1");
|
|
Assert.Equal(1, first.ScriptErrorCount);
|
|
Assert.Equal(1, first.AlarmEvaluationErrorCount);
|
|
Assert.Equal(1, first.DeadLetterCount);
|
|
|
|
var second = _collector.CollectReport("site-1");
|
|
Assert.Equal(0, second.ScriptErrorCount);
|
|
Assert.Equal(0, second.AlarmEvaluationErrorCount);
|
|
Assert.Equal(0, second.DeadLetterCount);
|
|
}
|
|
|
|
/// <summary>Verifies that a connection health update is reflected in the report.</summary>
|
|
[Fact]
|
|
public void UpdateConnectionHealth_ReflectedInReport()
|
|
{
|
|
_collector.UpdateConnectionHealth("opc-1", ConnectionHealth.Connected);
|
|
_collector.UpdateConnectionHealth("opc-2", ConnectionHealth.Disconnected);
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
|
|
Assert.Equal(2, report.DataConnectionStatuses.Count);
|
|
Assert.Equal(ConnectionHealth.Connected, report.DataConnectionStatuses["opc-1"]);
|
|
Assert.Equal(ConnectionHealth.Disconnected, report.DataConnectionStatuses["opc-2"]);
|
|
}
|
|
|
|
/// <summary>Verifies that connection health is not reset after a report is collected.</summary>
|
|
[Fact]
|
|
public void ConnectionHealth_NotResetAfterCollect()
|
|
{
|
|
_collector.UpdateConnectionHealth("opc-1", ConnectionHealth.Connected);
|
|
|
|
_collector.CollectReport("site-1");
|
|
var second = _collector.CollectReport("site-1");
|
|
|
|
Assert.Single(second.DataConnectionStatuses);
|
|
Assert.Equal(ConnectionHealth.Connected, second.DataConnectionStatuses["opc-1"]);
|
|
}
|
|
|
|
/// <summary>Verifies that removing a connection removes it from the report.</summary>
|
|
[Fact]
|
|
public void RemoveConnection_RemovesFromReport()
|
|
{
|
|
_collector.UpdateConnectionHealth("opc-1", ConnectionHealth.Connected);
|
|
_collector.UpdateTagResolution("opc-1", 10, 8);
|
|
_collector.RemoveConnection("opc-1");
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
Assert.Empty(report.DataConnectionStatuses);
|
|
Assert.Empty(report.TagResolutionCounts);
|
|
}
|
|
|
|
/// <summary>Verifies that a tag resolution update is reflected in the report.</summary>
|
|
[Fact]
|
|
public void UpdateTagResolution_ReflectedInReport()
|
|
{
|
|
_collector.UpdateTagResolution("opc-1", 50, 45);
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
|
|
Assert.Single(report.TagResolutionCounts);
|
|
Assert.Equal(50, report.TagResolutionCounts["opc-1"].TotalSubscribed);
|
|
Assert.Equal(45, report.TagResolutionCounts["opc-1"].SuccessfullyResolved);
|
|
}
|
|
|
|
/// <summary>Verifies that store-and-forward buffer depths default to empty when the setter was never called.</summary>
|
|
[Fact]
|
|
public void StoreAndForwardBufferDepths_DefaultsToEmpty_WhenSetterNotCalled()
|
|
{
|
|
var report = _collector.CollectReport("site-1");
|
|
Assert.Empty(report.StoreAndForwardBufferDepths);
|
|
}
|
|
|
|
/// <summary>Verifies that a report includes a UTC timestamp.</summary>
|
|
[Fact]
|
|
public void CollectReport_IncludesUtcTimestamp()
|
|
{
|
|
var before = DateTimeOffset.UtcNow;
|
|
var report = _collector.CollectReport("site-1");
|
|
var after = DateTimeOffset.UtcNow;
|
|
|
|
Assert.InRange(report.ReportTimestamp, before, after);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Regression: <see cref="SiteHealthCollector.CollectReport"/>
|
|
/// must stamp <c>ReportTimestamp</c> from an injected <see cref="TimeProvider"/>
|
|
/// (consistent with the rest of the module), not directly from
|
|
/// <c>DateTimeOffset.UtcNow</c>, so the report timestamp is deterministically
|
|
/// testable against a known instant.
|
|
/// </summary>
|
|
[Fact]
|
|
public void CollectReport_StampsTimestamp_FromInjectedTimeProvider()
|
|
{
|
|
var fixedInstant = new DateTimeOffset(2026, 5, 17, 9, 30, 0, TimeSpan.Zero);
|
|
var collector = new SiteHealthCollector(new TestTimeProvider(fixedInstant));
|
|
|
|
var report = collector.CollectReport("site-1");
|
|
|
|
Assert.Equal(fixedInstant, report.ReportTimestamp);
|
|
}
|
|
|
|
/// <summary>Verifies that a report sequence number is zero because the caller assigns it.</summary>
|
|
[Fact]
|
|
public void CollectReport_SequenceNumberIsZero_CallerAssignsIt()
|
|
{
|
|
var report = _collector.CollectReport("site-1");
|
|
Assert.Equal(0, report.SequenceNumber);
|
|
}
|
|
|
|
// Regression coverage: the remaining collector setters had no
|
|
// "reflected in report" coverage. The following tests verify each setter's
|
|
// value reaches CollectReport output.
|
|
|
|
/// <summary>Verifies that the cluster nodes are reflected in the report.</summary>
|
|
[Fact]
|
|
public void SetClusterNodes_ReflectedInReport()
|
|
{
|
|
var nodes = new List<ZB.MOM.WW.ScadaBridge.Commons.Messages.Health.NodeStatus>
|
|
{
|
|
new("node-a", true, "Active"),
|
|
new("node-b", true, "Standby")
|
|
};
|
|
_collector.SetClusterNodes(nodes);
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
|
|
Assert.NotNull(report.ClusterNodes);
|
|
Assert.Equal(2, report.ClusterNodes!.Count);
|
|
Assert.Equal("node-a", report.ClusterNodes[0].Hostname);
|
|
}
|
|
|
|
/// <summary>Verifies that the instance counts are reflected in the report.</summary>
|
|
[Fact]
|
|
public void SetInstanceCounts_ReflectedInReport()
|
|
{
|
|
_collector.SetInstanceCounts(deployed: 10, enabled: 7, disabled: 3);
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
|
|
Assert.Equal(10, report.DeployedInstanceCount);
|
|
Assert.Equal(7, report.EnabledInstanceCount);
|
|
Assert.Equal(3, report.DisabledInstanceCount);
|
|
}
|
|
|
|
/// <summary>Verifies that the parked message count is reflected in the report.</summary>
|
|
[Fact]
|
|
public void SetParkedMessageCount_ReflectedInReport()
|
|
{
|
|
_collector.SetParkedMessageCount(42);
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
|
|
Assert.Equal(42, report.ParkedMessageCount);
|
|
}
|
|
|
|
/// <summary>Verifies that the node hostname is reflected in the report.</summary>
|
|
[Fact]
|
|
public void SetNodeHostname_ReflectedInReport()
|
|
{
|
|
_collector.SetNodeHostname("site-host-1");
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
|
|
Assert.Equal("site-host-1", report.NodeHostname);
|
|
}
|
|
|
|
/// <summary>Verifies that the active-node setting is reflected in the node role.</summary>
|
|
[Fact]
|
|
public void SetActiveNode_ReflectedInNodeRole()
|
|
{
|
|
_collector.SetActiveNode(true);
|
|
Assert.Equal("Active", _collector.CollectReport("site-1").NodeRole);
|
|
Assert.True(_collector.IsActiveNode);
|
|
|
|
_collector.SetActiveNode(false);
|
|
Assert.Equal("Standby", _collector.CollectReport("site-1").NodeRole);
|
|
Assert.False(_collector.IsActiveNode);
|
|
}
|
|
|
|
/// <summary>Verifies that a tag quality update is reflected in the report.</summary>
|
|
[Fact]
|
|
public void UpdateTagQuality_ReflectedInReport()
|
|
{
|
|
_collector.UpdateTagQuality("opc-1", good: 80, bad: 15, uncertain: 5);
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
|
|
Assert.NotNull(report.DataConnectionTagQuality);
|
|
var quality = report.DataConnectionTagQuality!["opc-1"];
|
|
Assert.Equal(80, quality.Good);
|
|
Assert.Equal(15, quality.Bad);
|
|
Assert.Equal(5, quality.Uncertain);
|
|
}
|
|
|
|
/// <summary>Verifies that a connection endpoint update is reflected in the report.</summary>
|
|
[Fact]
|
|
public void UpdateConnectionEndpoint_ReflectedInReport()
|
|
{
|
|
_collector.UpdateConnectionEndpoint("opc-1", "opc.tcp://plc-1:4840");
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
|
|
Assert.NotNull(report.DataConnectionEndpoints);
|
|
Assert.Equal("opc.tcp://plc-1:4840", report.DataConnectionEndpoints!["opc-1"]);
|
|
}
|
|
|
|
/// <summary>Verifies that the store-and-forward depths are reflected in the report.</summary>
|
|
[Fact]
|
|
public void SetStoreAndForwardDepths_ReflectedInReport()
|
|
{
|
|
_collector.SetStoreAndForwardDepths(new Dictionary<string, int>
|
|
{
|
|
["ExternalSystem"] = 5,
|
|
["Notification"] = 2
|
|
});
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
|
|
Assert.Equal(5, report.StoreAndForwardBufferDepths["ExternalSystem"]);
|
|
Assert.Equal(2, report.StoreAndForwardBufferDepths["Notification"]);
|
|
}
|
|
|
|
/// <summary>Verifies that concurrent increments are counted without loss.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ThreadSafety_ConcurrentIncrements()
|
|
{
|
|
const int iterations = 10_000;
|
|
var tasks = new[]
|
|
{
|
|
Task.Run(() => { for (int i = 0; i < iterations; i++) _collector.IncrementScriptError(); }),
|
|
Task.Run(() => { for (int i = 0; i < iterations; i++) _collector.IncrementAlarmError(); }),
|
|
Task.Run(() => { for (int i = 0; i < iterations; i++) _collector.IncrementDeadLetter(); })
|
|
};
|
|
|
|
await Task.WhenAll(tasks);
|
|
|
|
var report = _collector.CollectReport("site-1");
|
|
Assert.Equal(iterations, report.ScriptErrorCount);
|
|
Assert.Equal(iterations, report.AlarmEvaluationErrorCount);
|
|
Assert.Equal(iterations, report.DeadLetterCount);
|
|
}
|
|
}
|