Files
scadalink-design/tests/ScadaLink.HealthMonitoring.Tests/SiteHealthCollectorTests.cs

283 lines
9.1 KiB
C#

using ScadaLink.Commons.Types.Enums;
namespace ScadaLink.HealthMonitoring.Tests;
public class SiteHealthCollectorTests
{
private readonly SiteHealthCollector _collector = new();
[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);
}
[Fact]
public void IncrementScriptError_AccumulatesBetweenReports()
{
_collector.IncrementScriptError();
_collector.IncrementScriptError();
_collector.IncrementScriptError();
var report = _collector.CollectReport("site-1");
Assert.Equal(3, report.ScriptErrorCount);
}
[Fact]
public void IncrementAlarmError_AccumulatesBetweenReports()
{
_collector.IncrementAlarmError();
_collector.IncrementAlarmError();
var report = _collector.CollectReport("site-1");
Assert.Equal(2, report.AlarmEvaluationErrorCount);
}
[Fact]
public void IncrementDeadLetter_AccumulatesBetweenReports()
{
_collector.IncrementDeadLetter();
var report = _collector.CollectReport("site-1");
Assert.Equal(1, report.DeadLetterCount);
}
[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);
}
[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"]);
}
[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"]);
}
[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);
}
[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);
}
[Fact]
public void StoreAndForwardBufferDepths_IsEmptyPlaceholder()
{
var report = _collector.CollectReport("site-1");
Assert.Empty(report.StoreAndForwardBufferDepths);
}
[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>
/// HealthMonitoring-016 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);
}
[Fact]
public void CollectReport_SequenceNumberIsZero_CallerAssignsIt()
{
var report = _collector.CollectReport("site-1");
Assert.Equal(0, report.SequenceNumber);
}
// HealthMonitoring-009 regression: the remaining collector setters had no
// "reflected in report" coverage. The following tests verify each setter's
// value reaches CollectReport output.
[Fact]
public void SetClusterNodes_ReflectedInReport()
{
var nodes = new List<ScadaLink.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);
}
[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);
}
[Fact]
public void SetParkedMessageCount_ReflectedInReport()
{
_collector.SetParkedMessageCount(42);
var report = _collector.CollectReport("site-1");
Assert.Equal(42, report.ParkedMessageCount);
}
[Fact]
public void SetNodeHostname_ReflectedInReport()
{
_collector.SetNodeHostname("site-host-1");
var report = _collector.CollectReport("site-1");
Assert.Equal("site-host-1", report.NodeHostname);
}
[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);
}
[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);
}
[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"]);
}
[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"]);
}
[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);
}
}