26ff8d9b4f
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
299 lines
9.7 KiB
C#
299 lines
9.7 KiB
C#
using System.Diagnostics.Metrics;
|
|
using JdeScoping.DataSync.Telemetry;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Shouldly;
|
|
|
|
namespace JdeScoping.DataSync.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for DataSyncMetrics.
|
|
/// Tests counter increments and histogram recordings.
|
|
/// </summary>
|
|
public class DataSyncMetricsTests : IDisposable
|
|
{
|
|
private readonly MeterListener _meterListener;
|
|
private readonly DataSyncMetrics _sut;
|
|
private readonly List<Measurement<long>> _longMeasurements = [];
|
|
private readonly List<Measurement<double>> _doubleMeasurements = [];
|
|
|
|
public DataSyncMetricsTests()
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddMetrics();
|
|
var provider = services.BuildServiceProvider();
|
|
var meterFactory = provider.GetRequiredService<IMeterFactory>();
|
|
|
|
_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<long>(OnMeasurementRecorded);
|
|
_meterListener.SetMeasurementEventCallback<double>(OnDoubleMeasurementRecorded);
|
|
_meterListener.Start();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_meterListener.Dispose();
|
|
}
|
|
|
|
private void OnMeasurementRecorded(
|
|
Instrument instrument,
|
|
long measurement,
|
|
ReadOnlySpan<KeyValuePair<string, object?>> tags,
|
|
object? state)
|
|
{
|
|
_longMeasurements.Add(new Measurement<long>(measurement, tags.ToArray()));
|
|
}
|
|
|
|
private void OnDoubleMeasurementRecorded(
|
|
Instrument instrument,
|
|
double measurement,
|
|
ReadOnlySpan<KeyValuePair<string, object?>> tags,
|
|
object? state)
|
|
{
|
|
_doubleMeasurements.Add(new Measurement<double>(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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a recorded measurement with its value and tags.
|
|
/// </summary>
|
|
/// <typeparam name="T">The measurement value type.</typeparam>
|
|
public struct Measurement<T>
|
|
{
|
|
public T Value { get; }
|
|
public KeyValuePair<string, object?>[] Tags { get; }
|
|
|
|
public Measurement(T value, KeyValuePair<string, object?>[] tags)
|
|
{
|
|
Value = value;
|
|
Tags = tags;
|
|
}
|
|
}
|