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;
}
}