using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ScadaLink.Commons.Messages.Health; using ScadaLink.Commons.Types.Enums; using ScadaLink.HealthMonitoring; namespace ScadaLink.PerformanceTests; /// /// WP-4 (Phase 8): Performance test framework for health reporting aggregation. /// Verifies health reporting from 10 sites can be aggregated correctly. /// public class HealthAggregationTests { private readonly CentralHealthAggregator _aggregator; public HealthAggregationTests() { var options = Options.Create(new HealthMonitoringOptions { ReportInterval = TimeSpan.FromSeconds(30), OfflineTimeout = TimeSpan.FromSeconds(60) }); _aggregator = new CentralHealthAggregator( options, NullLogger.Instance); } [Trait("Category", "Performance")] [Fact] public void AggregateHealthReports_10Sites_AllTracked() { const int siteCount = 10; for (var i = 0; i < siteCount; i++) { var siteId = $"site-{i + 1:D2}"; var report = new SiteHealthReport( SiteId: siteId, SequenceNumber: 1, ReportTimestamp: DateTimeOffset.UtcNow, DataConnectionStatuses: new Dictionary { [$"opc-{siteId}"] = ConnectionHealth.Connected }, TagResolutionCounts: new Dictionary { [$"opc-{siteId}"] = new(75, 72) }, ScriptErrorCount: 0, AlarmEvaluationErrorCount: 0, StoreAndForwardBufferDepths: new Dictionary { ["ext-system"] = i * 2 }, DeadLetterCount: 0, DeployedInstanceCount: 0, EnabledInstanceCount: 0, DisabledInstanceCount: 0); _aggregator.ProcessReport(report); } var states = _aggregator.GetAllSiteStates(); Assert.Equal(siteCount, states.Count); Assert.All(states.Values, s => Assert.True(s.IsOnline)); } [Trait("Category", "Performance")] [Fact] public void AggregateHealthReports_RapidUpdates_HandlesVolume() { const int siteCount = 10; const int updatesPerSite = 100; for (var seq = 1; seq <= updatesPerSite; seq++) { for (var s = 0; s < siteCount; s++) { var report = new SiteHealthReport( SiteId: $"site-{s + 1:D2}", SequenceNumber: seq, ReportTimestamp: DateTimeOffset.UtcNow, DataConnectionStatuses: new Dictionary(), TagResolutionCounts: new Dictionary(), ScriptErrorCount: seq % 5 == 0 ? 1 : 0, AlarmEvaluationErrorCount: 0, StoreAndForwardBufferDepths: new Dictionary(), DeadLetterCount: 0, DeployedInstanceCount: 0, EnabledInstanceCount: 0, DisabledInstanceCount: 0); _aggregator.ProcessReport(report); } } var states = _aggregator.GetAllSiteStates(); Assert.Equal(siteCount, states.Count); // Verify all sites have the latest sequence number Assert.All(states.Values, s => { Assert.Equal(updatesPerSite, s.LastSequenceNumber); Assert.True(s.IsOnline); }); } [Trait("Category", "Performance")] [Fact] public void AggregateHealthReports_StaleReportsRejected() { var siteId = "site-01"; // Send report with seq 10 _aggregator.ProcessReport(new SiteHealthReport( siteId, 10, DateTimeOffset.UtcNow, new Dictionary(), new Dictionary(), 5, 0, new Dictionary(), 0, 0, 0, 0)); // Send stale report with seq 5 — should be rejected _aggregator.ProcessReport(new SiteHealthReport( siteId, 5, DateTimeOffset.UtcNow, new Dictionary(), new Dictionary(), 99, 0, new Dictionary(), 0, 0, 0, 0)); var state = _aggregator.GetSiteState(siteId); Assert.NotNull(state); Assert.Equal(10, state!.LastSequenceNumber); // The script error count from report 10 (5) should be kept, not replaced by 99 Assert.Equal(5, state.LatestReport!.ScriptErrorCount); } [Trait("Category", "Performance")] [Fact] public void HealthCollector_CollectReport_ResetsIntervalCounters() { var collector = new SiteHealthCollector(); // Simulate errors during an interval for (var i = 0; i < 10; i++) collector.IncrementScriptError(); for (var i = 0; i < 3; i++) collector.IncrementAlarmError(); for (var i = 0; i < 7; i++) collector.IncrementDeadLetter(); collector.UpdateConnectionHealth("opc-1", ConnectionHealth.Connected); collector.UpdateTagResolution("opc-1", 75, 72); var report = collector.CollectReport("site-01"); Assert.Equal("site-01", report.SiteId); Assert.Equal(10, report.ScriptErrorCount); Assert.Equal(3, report.AlarmEvaluationErrorCount); Assert.Equal(7, report.DeadLetterCount); Assert.Single(report.DataConnectionStatuses); // Second collect should have reset interval counters var report2 = collector.CollectReport("site-01"); Assert.Equal(0, report2.ScriptErrorCount); Assert.Equal(0, report2.AlarmEvaluationErrorCount); Assert.Equal(0, report2.DeadLetterCount); // Connection status persists (not interval-based) Assert.Single(report2.DataConnectionStatuses); } }