Resolve all 46 low-severity architecture review findings
This commit is contained in:
@@ -11,6 +11,14 @@ public class KpiHistoryOptionsValidatorTests
|
||||
{
|
||||
private static KpiHistoryOptionsValidator NewValidator() => new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_SourceTimeout_Zero_Fails()
|
||||
{
|
||||
var result = NewValidator().Validate(null, new KpiHistoryOptions { SourceTimeout = TimeSpan.Zero });
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains("SourceTimeout", result.FailureMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Defaults_Pass()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi;
|
||||
@@ -470,6 +471,7 @@ public class KpiHistoryRecorderActorTests : TestKit
|
||||
IKpiHistoryRepository repository, params IKpiSampleSource[] sources)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddKpiHistory(new ConfigurationBuilder().Build());
|
||||
// Mirror production: sources + repository are scoped, so the recorder opens a fresh
|
||||
// scope per tick and resolves there.
|
||||
foreach (var source in sources)
|
||||
@@ -519,6 +521,80 @@ public class KpiHistoryRecorderActorTests : TestKit
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class HangingState
|
||||
{
|
||||
public readonly TaskCompletionSource<IReadOnlyList<KpiSample>> Completion = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public int Calls;
|
||||
public int ActiveScopesDisposed;
|
||||
}
|
||||
|
||||
private sealed class HangingSource(HangingState state) : IKpiSampleSource, IDisposable
|
||||
{
|
||||
private bool _called;
|
||||
public string Source => "hung";
|
||||
public Task<IReadOnlyList<KpiSample>> CollectAsync(DateTime capturedAtUtc, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_called = true;
|
||||
Interlocked.Increment(ref state.Calls);
|
||||
return state.Completion.Task; // deliberately ignores cancellation
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
if (_called) Interlocked.Increment(ref state.ActiveScopesDisposed);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void HungSource_DoesNotBlockHealthySourcesOrLoseScopeOwnership(bool replaceActor)
|
||||
{
|
||||
var state = new HangingState();
|
||||
var repository = new RecordingRepository();
|
||||
var services = new ServiceCollection();
|
||||
services.AddKpiHistory(new ConfigurationBuilder().Build());
|
||||
services.AddScoped<IKpiSampleSource>(_ => new HangingSource(state));
|
||||
services.AddScoped<IKpiSampleSource>(_ => new HealthySource());
|
||||
services.AddScoped<IKpiHistoryRepository>(_ => repository);
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var options = new KpiHistoryOptions
|
||||
{
|
||||
SourceTimeout = TimeSpan.FromMilliseconds(50),
|
||||
SampleInterval = TimeSpan.FromHours(1),
|
||||
};
|
||||
var actor = CreateActor(provider, options);
|
||||
try
|
||||
{
|
||||
actor.Tell(KpiHistoryRecorderActor.SampleTick.Instance);
|
||||
AwaitAssert(() => Assert.Equal(2, repository.Recorded.Count), TimeSpan.FromSeconds(2));
|
||||
if (replaceActor)
|
||||
{
|
||||
Watch(actor);
|
||||
Sys.Stop(actor);
|
||||
ExpectTerminated(actor);
|
||||
Assert.Equal(0, Volatile.Read(ref state.ActiveScopesDisposed));
|
||||
actor = CreateActor(provider, options);
|
||||
}
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
actor.Tell(KpiHistoryRecorderActor.SampleTick.Instance);
|
||||
Assert.True(repository.Recorded.Count >= 4);
|
||||
}, TimeSpan.FromSeconds(2));
|
||||
Assert.Equal(1, Volatile.Read(ref state.Calls));
|
||||
Assert.Equal(0, Volatile.Read(ref state.ActiveScopesDisposed));
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Stop before releasing the source so no queued sampling tick can legitimately
|
||||
// begin a replacement after it finishes and race the disposal assertion.
|
||||
Watch(actor);
|
||||
Sys.Stop(actor);
|
||||
ExpectTerminated(actor);
|
||||
state.Completion.TrySetResult(Array.Empty<KpiSample>());
|
||||
}
|
||||
AwaitAssert(() => Assert.Equal(1, Volatile.Read(ref state.ActiveScopesDisposed)), TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SampleTick_WritesHealthySourceSamples_AndThrowingSourceDoesNotAbortTick()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user