using System; using Shouldly; using Xunit; using ZB.MOM.WW.LmxOpcUa.Host.Metrics; namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics { /// /// Verifies operation timing aggregation, rolling buffers, and success tracking used by the bridge metrics subsystem. /// public class PerformanceMetricsTests { /// /// Confirms that a fresh metrics collector reports no statistics. /// [Fact] public void EmptyState_ReturnsZeroStatistics() { using var metrics = new PerformanceMetrics(); var stats = metrics.GetStatistics(); stats.ShouldBeEmpty(); } /// /// Confirms that repeated operation recordings update total and successful execution counts. /// [Fact] public void RecordOperation_TracksCounts() { using var metrics = new PerformanceMetrics(); metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10), true); metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(20), false); var stats = metrics.GetStatistics(); stats.ShouldContainKey("Read"); stats["Read"].TotalCount.ShouldBe(2); stats["Read"].SuccessCount.ShouldBe(1); stats["Read"].SuccessRate.ShouldBe(0.5); } /// /// Confirms that min, max, and average timing values are calculated from recorded operations. /// [Fact] public void RecordOperation_TracksMinMaxAverage() { using var metrics = new PerformanceMetrics(); metrics.RecordOperation("Write", TimeSpan.FromMilliseconds(10)); metrics.RecordOperation("Write", TimeSpan.FromMilliseconds(30)); metrics.RecordOperation("Write", TimeSpan.FromMilliseconds(20)); var stats = metrics.GetStatistics()["Write"]; stats.MinMilliseconds.ShouldBe(10); stats.MaxMilliseconds.ShouldBe(30); stats.AverageMilliseconds.ShouldBe(20); } /// /// Confirms that the 95th percentile is calculated from the recorded timing sample. /// [Fact] public void P95_CalculatedCorrectly() { using var metrics = new PerformanceMetrics(); for (int i = 1; i <= 100; i++) metrics.RecordOperation("Op", TimeSpan.FromMilliseconds(i)); var stats = metrics.GetStatistics()["Op"]; stats.Percentile95Milliseconds.ShouldBe(95); } /// /// Confirms that the rolling buffer keeps the most recent operation durations for percentile calculations. /// [Fact] public void RollingBuffer_EvictsOldEntries() { var opMetrics = new OperationMetrics(); for (int i = 0; i < 1100; i++) opMetrics.Record(TimeSpan.FromMilliseconds(i), true); var stats = opMetrics.GetStatistics(); stats.TotalCount.ShouldBe(1100); // P95 should be from the last 1000 entries (100-1099) stats.Percentile95Milliseconds.ShouldBeGreaterThan(1000); } /// /// Confirms that a timing scope records an operation when disposed. /// [Fact] public void BeginOperation_TimingScopeRecordsOnDispose() { using var metrics = new PerformanceMetrics(); using (var scope = metrics.BeginOperation("Test")) { // Simulate some work System.Threading.Thread.Sleep(5); } var stats = metrics.GetStatistics(); stats.ShouldContainKey("Test"); stats["Test"].TotalCount.ShouldBe(1); stats["Test"].SuccessCount.ShouldBe(1); stats["Test"].AverageMilliseconds.ShouldBeGreaterThan(0); } /// /// Confirms that a timing scope can mark an operation as failed before disposal. /// [Fact] public void BeginOperation_SetSuccessFalse() { using var metrics = new PerformanceMetrics(); using (var scope = metrics.BeginOperation("Test")) { scope.SetSuccess(false); } var stats = metrics.GetStatistics()["Test"]; stats.TotalCount.ShouldBe(1); stats.SuccessCount.ShouldBe(0); } /// /// Confirms that looking up an unknown operation returns no metrics bucket. /// [Fact] public void GetMetrics_UnknownOperation_ReturnsNull() { using var metrics = new PerformanceMetrics(); metrics.GetMetrics("NonExistent").ShouldBeNull(); } /// /// Confirms that operation names are tracked without case sensitivity. /// [Fact] public void OperationNames_AreCaseInsensitive() { using var metrics = new PerformanceMetrics(); metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10)); metrics.RecordOperation("read", TimeSpan.FromMilliseconds(20)); var stats = metrics.GetStatistics(); stats.Count.ShouldBe(1); stats["READ"].TotalCount.ShouldBe(2); } } }