Files
Joseph Doherty 26ff8d9b4f Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
2026-01-02 07:43:29 -05:00

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