using System.Diagnostics.Metrics; using JdeScoping.DataSync.Telemetry; using Microsoft.Extensions.DependencyInjection; using Shouldly; namespace JdeScoping.DataSync.Tests; /// /// Unit tests for DataSyncMetrics. /// Tests counter increments and histogram recordings. /// public class DataSyncMetricsTests : IDisposable { private readonly MeterListener _meterListener; private readonly DataSyncMetrics _sut; private readonly List> _longMeasurements = []; private readonly List> _doubleMeasurements = []; public DataSyncMetricsTests() { var services = new ServiceCollection(); services.AddMetrics(); var provider = services.BuildServiceProvider(); var meterFactory = provider.GetRequiredService(); _sut = new DataSyncMetrics(meterFactory); // Set up meter listener to capture measurements _meterListener = new MeterListener(); _meterListener.InstrumentPublished = (instrument, listener) => { if (instrument.Meter.Name == "JdeScoping.DataSync") { listener.EnableMeasurementEvents(instrument); } }; _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded); _meterListener.SetMeasurementEventCallback(OnDoubleMeasurementRecorded); _meterListener.Start(); } public void Dispose() { _meterListener.Dispose(); } private void OnMeasurementRecorded( Instrument instrument, long measurement, ReadOnlySpan> tags, object? state) { _longMeasurements.Add(new Measurement(measurement, tags.ToArray())); } private void OnDoubleMeasurementRecorded( Instrument instrument, double measurement, ReadOnlySpan> tags, object? state) { _doubleMeasurements.Add(new Measurement(measurement, tags.ToArray())); } #region Operation Started Counter Tests [Fact] public void RecordOperationStarted_IncrementsCounter() { // Act _sut.RecordOperationStarted("WorkOrder", "Mass"); _meterListener.RecordObservableInstruments(); // Assert var measurement = _longMeasurements.FirstOrDefault(m => m.Tags.Any(t => t.Key == "table" && t.Value?.ToString() == "WorkOrder") && m.Tags.Any(t => t.Key == "type" && t.Value?.ToString() == "Mass")); measurement.Value.ShouldBe(1); } [Fact] public void RecordOperationStarted_MultipleCalls_AccumulatesCount() { // Act _sut.RecordOperationStarted("WorkOrder", "Mass"); _sut.RecordOperationStarted("WorkOrder", "Mass"); _sut.RecordOperationStarted("LotUsage", "Daily"); _meterListener.RecordObservableInstruments(); // Assert var workOrderMeasurements = _longMeasurements .Where(m => m.Tags.Any(t => t.Key == "table" && t.Value?.ToString() == "WorkOrder")) .ToList(); workOrderMeasurements.Count.ShouldBe(2); } [Fact] public void RecordOperationStarted_DifferentTables_TrackedSeparately() { // Act _sut.RecordOperationStarted("WorkOrder", "Mass"); _sut.RecordOperationStarted("LotUsage", "Daily"); _meterListener.RecordObservableInstruments(); // Assert _longMeasurements.Any(m => m.Tags.Any(t => t.Key == "table" && (t.Value as string) == "WorkOrder")).ShouldBeTrue(); _longMeasurements.Any(m => m.Tags.Any(t => t.Key == "table" && (t.Value as string) == "LotUsage")).ShouldBeTrue(); } #endregion #region Operation Completed Counter Tests [Fact] public void RecordOperationCompleted_IncrementsCounterAndRecordsHistograms() { // Act _sut.RecordOperationCompleted("WorkOrder", "Mass", recordCount: 5000, durationSeconds: 12.5); _meterListener.RecordObservableInstruments(); // Assert: Counter incremented var counterMeasurement = _longMeasurements.FirstOrDefault(m => m.Tags.Any(t => t.Key == "table" && t.Value?.ToString() == "WorkOrder")); counterMeasurement.Value.ShouldBe(1); // Assert: Duration histogram recorded var durationMeasurement = _doubleMeasurements.FirstOrDefault(m => m.Tags.Any(t => t.Key == "table" && t.Value?.ToString() == "WorkOrder")); durationMeasurement.Value.ShouldBe(12.5); // Assert: Records histogram recorded var recordsMeasurement = _longMeasurements.FirstOrDefault(m => m.Value == 5000); recordsMeasurement.Value.ShouldBe(5000); } [Fact] public void RecordOperationCompleted_WithZeroRecords_StillRecords() { // Act _sut.RecordOperationCompleted("Item", "Hourly", recordCount: 0, durationSeconds: 0.5); _meterListener.RecordObservableInstruments(); // Assert _longMeasurements.ShouldContain(m => m.Value == 0); } [Fact] public void RecordOperationCompleted_WithLargeRecordCount_HandlesCorrectly() { // Act _sut.RecordOperationCompleted("WorkOrder", "Mass", recordCount: 10_000_000, durationSeconds: 300.0); _meterListener.RecordObservableInstruments(); // Assert _longMeasurements.ShouldContain(m => m.Value == 10_000_000); } #endregion #region Operation Failed Counter Tests [Fact] public void RecordOperationFailed_IncrementsCounter() { // Act _sut.RecordOperationFailed("WorkOrder", "Daily"); _meterListener.RecordObservableInstruments(); // Assert var measurement = _longMeasurements.FirstOrDefault(m => m.Tags.Any(t => t.Key == "table" && t.Value?.ToString() == "WorkOrder") && m.Tags.Any(t => t.Key == "type" && t.Value?.ToString() == "Daily")); measurement.Value.ShouldBe(1); } [Fact] public void RecordOperationFailed_MultipleFailures_AccumulatesCount() { // Act _sut.RecordOperationFailed("WorkOrder", "Daily"); _sut.RecordOperationFailed("WorkOrder", "Daily"); _sut.RecordOperationFailed("WorkOrder", "Daily"); _meterListener.RecordObservableInstruments(); // Assert var failedMeasurements = _longMeasurements .Where(m => m.Tags.Any(t => t.Key == "table" && t.Value?.ToString() == "WorkOrder")) .ToList(); failedMeasurements.Count.ShouldBe(3); } #endregion #region Cycle Error Counter Tests [Fact] public void RecordCycleError_IncrementsCounter() { // Act _sut.RecordCycleError(); _meterListener.RecordObservableInstruments(); // Assert _longMeasurements.ShouldContain(m => m.Value == 1); } #endregion #region Cycle Completed Counter Tests [Fact] public void RecordCycleCompleted_IncrementsCounterWithTags() { // Act _sut.RecordCycleCompleted(successCount: 5, failedCount: 2, durationSeconds: 45.0); _meterListener.RecordObservableInstruments(); // Assert var measurement = _longMeasurements.FirstOrDefault(m => m.Tags.Any(t => t.Key == "success_count" && (int)t.Value! == 5) && m.Tags.Any(t => t.Key == "failed_count" && (int)t.Value! == 2)); measurement.Value.ShouldBe(1); } [Fact] public void RecordCycleCompleted_AllSuccessful_RecordsCorrectly() { // Act _sut.RecordCycleCompleted(successCount: 10, failedCount: 0, durationSeconds: 30.0); _meterListener.RecordObservableInstruments(); // Assert _longMeasurements.ShouldContain(m => m.Tags.Any(t => t.Key == "success_count" && (int)t.Value! == 10) && m.Tags.Any(t => t.Key == "failed_count" && (int)t.Value! == 0)); } [Fact] public void RecordCycleCompleted_AllFailed_RecordsCorrectly() { // Act _sut.RecordCycleCompleted(successCount: 0, failedCount: 5, durationSeconds: 10.0); _meterListener.RecordObservableInstruments(); // Assert _longMeasurements.ShouldContain(m => m.Tags.Any(t => t.Key == "success_count" && (int)t.Value! == 0) && m.Tags.Any(t => t.Key == "failed_count" && (int)t.Value! == 5)); } #endregion #region Tag Verification Tests [Fact] public void AllOperationMetrics_IncludeTableAndTypeTags() { // Act _sut.RecordOperationStarted("TestTable", "TestType"); _sut.RecordOperationCompleted("TestTable", "TestType", 100, 1.0); _sut.RecordOperationFailed("TestTable", "TestType"); _meterListener.RecordObservableInstruments(); // Assert: All measurements should have both table and type tags foreach (var measurement in _longMeasurements.Take(3)) // First 3 are from the calls above { measurement.Tags.ShouldContain(t => t.Key == "table"); measurement.Tags.ShouldContain(t => t.Key == "type"); } } #endregion } /// /// Represents a recorded measurement with its value and tags. /// /// The measurement value type. public struct Measurement { public T Value { get; } public KeyValuePair[] Tags { get; } public Measurement(T value, KeyValuePair[] tags) { Value = value; Tags = tags; } }