using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi; namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Kpi; /// /// Tests for — the gauge-vs-rate /// classification the hourly rollup fold consults. Covers totality over every /// metric the four IKpiSampleSource implementations actually emit, the /// specific Rate set, a gauge sampling, and the unknown-metric default. /// public class KpiMetricAggregationTests { // Every (source, metric) pair emitted by the four sample sources, transcribed // from the source code. The catalog must classify all of them (totality). public static readonly (string Source, string Metric)[] AllEmittedMetrics = { // NotificationOutboxKpiSampleSource (KpiSources.NotificationOutbox, "queueDepth"), (KpiSources.NotificationOutbox, "stuckCount"), (KpiSources.NotificationOutbox, "parkedCount"), (KpiSources.NotificationOutbox, "deliveredLastInterval"), (KpiSources.NotificationOutbox, "oldestPendingAgeSeconds"), // SiteCallAuditKpiSampleSource (KpiSources.SiteCallAudit, "buffered"), (KpiSources.SiteCallAudit, "parked"), (KpiSources.SiteCallAudit, "failedLastInterval"), (KpiSources.SiteCallAudit, "deliveredLastInterval"), (KpiSources.SiteCallAudit, "stuck"), (KpiSources.SiteCallAudit, "oldestPendingAgeSeconds"), // AuditLogKpiSampleSource (KpiSources.AuditLog, "totalEventsLastHour"), (KpiSources.AuditLog, "errorEventsLastHour"), (KpiSources.AuditLog, "backlogTotal"), // SiteHealthKpiSampleSource (KpiSources.SiteHealth, "connectionsUp"), (KpiSources.SiteHealth, "connectionsDown"), (KpiSources.SiteHealth, "scriptErrors"), (KpiSources.SiteHealth, "alarmEvalErrors"), (KpiSources.SiteHealth, "sfBufferDepth"), (KpiSources.SiteHealth, "deadLetters"), (KpiSources.SiteHealth, "parkedMessages"), (KpiSources.SiteHealth, "deployedInstances"), (KpiSources.SiteHealth, "enabledInstances"), (KpiSources.SiteHealth, "disabledInstances"), (KpiSources.SiteHealth, "auditBacklogPending"), (KpiSources.SiteHealth, "eventLogWriteFailures"), }; // The (source, metric) pairs that must fold as Rate (sum-per-hour). public static readonly (string Source, string Metric)[] RateMetrics = { (KpiSources.NotificationOutbox, "deliveredLastInterval"), (KpiSources.SiteCallAudit, "failedLastInterval"), (KpiSources.SiteCallAudit, "deliveredLastInterval"), (KpiSources.SiteHealth, "scriptErrors"), (KpiSources.SiteHealth, "alarmEvalErrors"), (KpiSources.SiteHealth, "deadLetters"), (KpiSources.SiteHealth, "eventLogWriteFailures"), }; // A representative sampling of gauges — including the two *LastHour audit // metrics that are rolling last-hour gauges, NOT rates (deliberate override). public static readonly (string Source, string Metric)[] GaugeSampling = { (KpiSources.NotificationOutbox, "queueDepth"), (KpiSources.NotificationOutbox, "oldestPendingAgeSeconds"), (KpiSources.SiteCallAudit, "buffered"), (KpiSources.SiteCallAudit, "stuck"), (KpiSources.AuditLog, "backlogTotal"), (KpiSources.AuditLog, "totalEventsLastHour"), (KpiSources.AuditLog, "errorEventsLastHour"), (KpiSources.SiteHealth, "connectionsUp"), (KpiSources.SiteHealth, "connectionsDown"), (KpiSources.SiteHealth, "sfBufferDepth"), (KpiSources.SiteHealth, "deployedInstances"), }; public static IEnumerable AllEmittedCases() => AllEmittedMetrics.Select(m => new object[] { m.Source, m.Metric }); public static IEnumerable RateCases() => RateMetrics.Select(m => new object[] { m.Source, m.Metric }); public static IEnumerable GaugeCases() => GaugeSampling.Select(m => new object[] { m.Source, m.Metric }); [Theory] [MemberData(nameof(AllEmittedCases))] public void Resolve_EveryEmittedMetric_ReturnsDefinedAggregation(string source, string metric) { var result = KpiMetricAggregationCatalog.Resolve(source, metric); Assert.True( result is KpiRollupAggregation.Gauge or KpiRollupAggregation.Rate, $"({source}, {metric}) resolved to an undefined aggregation: {result}"); } [Theory] [MemberData(nameof(RateCases))] public void Resolve_RateMetrics_ClassifiedAsRate(string source, string metric) { Assert.Equal(KpiRollupAggregation.Rate, KpiMetricAggregationCatalog.Resolve(source, metric)); } [Theory] [MemberData(nameof(GaugeCases))] public void Resolve_GaugeSampling_ClassifiedAsGauge(string source, string metric) { Assert.Equal(KpiRollupAggregation.Gauge, KpiMetricAggregationCatalog.Resolve(source, metric)); } [Fact] public void Resolve_LastHourAuditMetrics_AreGaugeNotRate() { // Rolling trailing-one-hour counts: last-value-per-hour is correct; summing // 60 overlapping minute samples would ~60x over-count. Guard the decision. Assert.Equal( KpiRollupAggregation.Gauge, KpiMetricAggregationCatalog.Resolve(KpiSources.AuditLog, "totalEventsLastHour")); Assert.Equal( KpiRollupAggregation.Gauge, KpiMetricAggregationCatalog.Resolve(KpiSources.AuditLog, "errorEventsLastHour")); } [Theory] [InlineData(KpiSources.NotificationOutbox, "someBrandNewMetricAddedLater")] [InlineData("AnEntirelyNewSource", "anyMetric")] [InlineData(KpiSources.SiteHealth, "")] public void Resolve_UnknownMetric_DefaultsToGauge(string source, string metric) { // The fold must never crash on a metric added after this catalog was updated; // Gauge (last-value) is the safe, non-fabricating default. Assert.Equal(KpiRollupAggregation.Gauge, KpiMetricAggregationCatalog.Resolve(source, metric)); } [Fact] public void Resolve_SameMetricNameDifferentSource_KeysOnSourcePair() { // deliveredLastInterval is Rate under both sources here, but the point is the // API keys on (source, metric) rather than metric alone — both resolve. Assert.Equal( KpiRollupAggregation.Rate, KpiMetricAggregationCatalog.Resolve(KpiSources.NotificationOutbox, "deliveredLastInterval")); Assert.Equal( KpiRollupAggregation.Rate, KpiMetricAggregationCatalog.Resolve(KpiSources.SiteCallAudit, "deliveredLastInterval")); } }