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.
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user