ec4c8fab87
Move configuration options from Core/DataAccess/DataSync/ExcelIO to dedicated Options folders within each project for better organization. Update all references and tests accordingly.
672 lines
22 KiB
C#
672 lines
22 KiB
C#
using System.Diagnostics.Metrics;
|
|
using JdeScoping.DataSync.Options;
|
|
using JdeScoping.DataSync.Contracts;
|
|
using JdeScoping.DataSync.Telemetry;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using Shouldly;
|
|
|
|
namespace JdeScoping.DataSync.Tests;
|
|
|
|
/// <summary>
|
|
/// Integration tests for DataSyncService.
|
|
/// These tests verify the service lifecycle and orchestration behavior.
|
|
/// </summary>
|
|
public class DataSyncServiceTests
|
|
{
|
|
#region Service Startup and Shutdown
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenDisabled_ExitsImmediately()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = false
|
|
});
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(Substitute.For<IDataUpdateRepository>());
|
|
services.AddSingleton(Substitute.For<ISyncOrchestrator>());
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
var task = service.StartAsync(cts.Token);
|
|
await Task.Delay(100); // Give it time to start
|
|
|
|
// Assert: Service should complete quickly since it's disabled
|
|
await service.StopAsync(CancellationToken.None);
|
|
task.IsCompleted.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenEnabled_StartsAndCanBeStopped()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromMilliseconds(100)
|
|
});
|
|
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
repository.CloseOpenUpdateEntriesAsync(Arg.Any<CancellationToken>())
|
|
.Returns(0);
|
|
|
|
var orchestratorCallCount = 0;
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
orchestrator.ExecutePendingSyncsAsync(Arg.Any<CancellationToken>())
|
|
.Returns(x =>
|
|
{
|
|
orchestratorCallCount++;
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(repository);
|
|
services.AddSingleton(orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(350); // Let it run a few cycles
|
|
|
|
cts.Cancel();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert: Should have called orchestrator at least once
|
|
orchestratorCallCount.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_GracefulShutdown_CompletesCleanly()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromSeconds(10) // Long interval
|
|
});
|
|
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(repository);
|
|
services.AddSingleton(orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
|
|
// Request cancellation after brief delay
|
|
await Task.Delay(50);
|
|
cts.Cancel();
|
|
|
|
// Should not throw and should complete
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert: No exceptions thrown during shutdown
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CloseOpenUpdateEntries at Startup
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_AtStartup_CallsCloseOpenUpdateEntries()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromMilliseconds(50)
|
|
});
|
|
|
|
var closeEntriesCallCount = 0;
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
repository.CloseOpenUpdateEntriesAsync(Arg.Any<CancellationToken>())
|
|
.Returns(x =>
|
|
{
|
|
closeEntriesCallCount++;
|
|
return Task.FromResult(0);
|
|
});
|
|
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(repository);
|
|
services.AddSingleton(orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(100);
|
|
cts.Cancel();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
closeEntriesCallCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenCloseOpenEntriesFindsEntries_LogsAndContinues()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromMilliseconds(50)
|
|
});
|
|
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
repository.CloseOpenUpdateEntriesAsync(Arg.Any<CancellationToken>())
|
|
.Returns(5); // Found 5 interrupted entries
|
|
|
|
var orchestratorCallCount = 0;
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
orchestrator.ExecutePendingSyncsAsync(Arg.Any<CancellationToken>())
|
|
.Returns(x =>
|
|
{
|
|
orchestratorCallCount++;
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(repository);
|
|
services.AddSingleton(orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(150);
|
|
cts.Cancel();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert: Should have continued to orchestrator after close
|
|
orchestratorCallCount.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenCloseOpenEntriesThrows_ContinuesStarting()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromMilliseconds(50)
|
|
});
|
|
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
repository.CloseOpenUpdateEntriesAsync(Arg.Any<CancellationToken>())
|
|
.Returns<int>(x => throw new Exception("Database error"));
|
|
|
|
var orchestratorCallCount = 0;
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
orchestrator.ExecutePendingSyncsAsync(Arg.Any<CancellationToken>())
|
|
.Returns(x =>
|
|
{
|
|
orchestratorCallCount++;
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(repository);
|
|
services.AddSingleton(orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act - Should not throw even if CloseOpenUpdateEntries fails
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(150);
|
|
cts.Cancel();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert: Should have continued and called orchestrator
|
|
orchestratorCallCount.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Parallel Sync Execution
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_CallsOrchestratorForParallelExecution()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromMilliseconds(50),
|
|
MaxDegreeOfParallelism = 4
|
|
});
|
|
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
|
|
var orchestratorCallCount = 0;
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
orchestrator.ExecutePendingSyncsAsync(Arg.Any<CancellationToken>())
|
|
.Returns(x =>
|
|
{
|
|
orchestratorCallCount++;
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(repository);
|
|
services.AddSingleton(orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(200); // Let multiple cycles run
|
|
cts.Cancel();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert: Orchestrator should be called to handle parallel execution
|
|
orchestratorCallCount.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenOrchestratorThrows_ContinuesNextCycle()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromMilliseconds(50)
|
|
});
|
|
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
|
|
var callCount = 0;
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
orchestrator.ExecutePendingSyncsAsync(Arg.Any<CancellationToken>())
|
|
.Returns(x =>
|
|
{
|
|
callCount++;
|
|
if (callCount == 1)
|
|
{
|
|
throw new Exception("Sync error");
|
|
}
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(repository);
|
|
services.AddSingleton(orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(250); // Let multiple cycles run
|
|
cts.Cancel();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert: Should have been called multiple times despite first failure
|
|
callCount.ShouldBeGreaterThan(1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cancellation Handling
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenCancelled_StopsGracefully()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromSeconds(10)
|
|
});
|
|
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
|
|
// Make orchestrator take some time but respect cancellation
|
|
orchestrator.ExecutePendingSyncsAsync(Arg.Any<CancellationToken>())
|
|
.Returns(async x =>
|
|
{
|
|
try
|
|
{
|
|
await Task.Delay(5000, x.Arg<CancellationToken>());
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Expected - swallow and return
|
|
}
|
|
});
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(repository);
|
|
services.AddSingleton(orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(100);
|
|
|
|
// Cancel while orchestrator is running
|
|
cts.Cancel();
|
|
|
|
// Should complete without hanging
|
|
var stopTask = service.StopAsync(CancellationToken.None);
|
|
var completed = await Task.WhenAny(stopTask, Task.Delay(2000));
|
|
|
|
// Assert: Should complete, not hang
|
|
completed.ShouldBe(stopTask);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_PassesCancellationTokenToOrchestrator()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromMilliseconds(50)
|
|
});
|
|
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
|
|
var tokenWasProvided = false;
|
|
orchestrator.ExecutePendingSyncsAsync(Arg.Any<CancellationToken>())
|
|
.Returns(x =>
|
|
{
|
|
var token = x.Arg<CancellationToken>();
|
|
tokenWasProvided = token != default;
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(repository);
|
|
services.AddSingleton(orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(100);
|
|
cts.Cancel();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert: Token should have been passed
|
|
tokenWasProvided.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenCancelledDuringDelay_ExitsCleanly()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromMinutes(5) // Long delay
|
|
});
|
|
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
orchestrator.ExecutePendingSyncsAsync(Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(repository);
|
|
services.AddSingleton(orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
|
|
// Service should be in delay after first cycle
|
|
await Task.Delay(100);
|
|
|
|
// Cancel during delay
|
|
cts.Cancel();
|
|
|
|
// Should exit quickly
|
|
var stopTask = service.StopAsync(CancellationToken.None);
|
|
var completed = await Task.WhenAny(stopTask, Task.Delay(1000));
|
|
|
|
// Assert
|
|
completed.ShouldBe(stopTask);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Service Scope Isolation
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_UsesNewScopePerCycle()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromMilliseconds(50)
|
|
});
|
|
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
|
|
var scopeCount = 0;
|
|
var services = new ServiceCollection();
|
|
services.AddScoped<IDataUpdateRepository>(sp =>
|
|
{
|
|
Interlocked.Increment(ref scopeCount);
|
|
return repository;
|
|
});
|
|
services.AddScoped<ISyncOrchestrator>(sp => orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(200); // Multiple cycles
|
|
cts.Cancel();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert: Multiple scopes should have been created
|
|
scopeCount.ShouldBeGreaterThan(1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Error Handling and Metrics
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenSyncFails_ContinuesRunning()
|
|
{
|
|
// Arrange
|
|
var options = Microsoft.Extensions.Options.Options.Create(new DataSyncOptions
|
|
{
|
|
Enabled = true,
|
|
CheckInterval = TimeSpan.FromMilliseconds(50)
|
|
});
|
|
|
|
var repository = Substitute.For<IDataUpdateRepository>();
|
|
|
|
var callCount = 0;
|
|
var orchestrator = Substitute.For<ISyncOrchestrator>();
|
|
orchestrator.ExecutePendingSyncsAsync(Arg.Any<CancellationToken>())
|
|
.Returns(x =>
|
|
{
|
|
callCount++;
|
|
throw new Exception("Sync failed");
|
|
});
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(repository);
|
|
services.AddSingleton(orchestrator);
|
|
var serviceProvider = services.BuildServiceProvider();
|
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
|
|
|
var metrics = CreateMetrics();
|
|
|
|
var service = new DataSyncService(
|
|
scopeFactory,
|
|
options,
|
|
NullLogger<DataSyncService>.Instance,
|
|
metrics);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
// Act
|
|
await service.StartAsync(cts.Token);
|
|
await Task.Delay(200);
|
|
cts.Cancel();
|
|
await service.StopAsync(CancellationToken.None);
|
|
|
|
// Assert: Should have continued calling orchestrator despite failures
|
|
callCount.ShouldBeGreaterThan(1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private static DataSyncMetrics CreateMetrics()
|
|
{
|
|
// Use real MeterFactory since mocking Meter is complex
|
|
var services = new ServiceCollection();
|
|
services.AddMetrics();
|
|
var provider = services.BuildServiceProvider();
|
|
var meterFactory = provider.GetRequiredService<IMeterFactory>();
|
|
return new DataSyncMetrics(meterFactory);
|
|
}
|
|
|
|
#endregion
|
|
}
|