109 lines
3.4 KiB
C#
109 lines
3.4 KiB
C#
using Omika.Core.Observability.External;
|
|
|
|
namespace Omika.Tests.Observability.External;
|
|
|
|
/// <summary>Disjoncteur du pont externe (§16) : déterministe (horloge injectée).</summary>
|
|
public class ExternalCircuitBreakerTests
|
|
{
|
|
[Fact]
|
|
public void StartsHealthy()
|
|
{
|
|
var circuit = NewCircuit();
|
|
|
|
Assert.Equal(ExternalObservabilityState.Healthy, circuit.State);
|
|
Assert.True(circuit.AllowExport());
|
|
}
|
|
|
|
[Fact]
|
|
public void OpensAfterThresholdFailures()
|
|
{
|
|
var circuit = NewCircuit(); // seuil = 3
|
|
|
|
circuit.RecordFailure();
|
|
circuit.RecordFailure();
|
|
Assert.Equal(ExternalObservabilityState.Degraded, circuit.State);
|
|
Assert.True(circuit.AllowExport()); // dégradé mais autorisé
|
|
|
|
circuit.RecordFailure();
|
|
Assert.Equal(ExternalObservabilityState.Open, circuit.State);
|
|
}
|
|
|
|
[Fact]
|
|
public void DropsWhileOpen()
|
|
{
|
|
var clock = new MutableClock();
|
|
var circuit = new ExternalCircuitBreaker(2, TimeSpan.FromSeconds(30), clock.Now);
|
|
circuit.RecordFailure();
|
|
circuit.RecordFailure();
|
|
|
|
Assert.False(circuit.AllowExport()); // open : drop
|
|
Assert.False(circuit.AllowExport());
|
|
}
|
|
|
|
[Fact]
|
|
public void ProbesAfterCooldown()
|
|
{
|
|
var clock = new MutableClock();
|
|
var circuit = new ExternalCircuitBreaker(1, TimeSpan.FromSeconds(30), clock.Now);
|
|
circuit.RecordFailure();
|
|
Assert.False(circuit.AllowExport());
|
|
|
|
clock.Advance(TimeSpan.FromSeconds(31));
|
|
Assert.True(circuit.AllowExport()); // half-open : autorise UNE sonde
|
|
Assert.Equal(ExternalObservabilityState.Degraded, circuit.State);
|
|
}
|
|
|
|
[Fact]
|
|
public void SuccessClosesCircuit()
|
|
{
|
|
var clock = new MutableClock();
|
|
var circuit = new ExternalCircuitBreaker(1, TimeSpan.FromSeconds(30), clock.Now);
|
|
circuit.RecordFailure();
|
|
clock.Advance(TimeSpan.FromSeconds(31));
|
|
Assert.True(circuit.AllowExport());
|
|
|
|
circuit.RecordSuccess();
|
|
Assert.Equal(ExternalObservabilityState.Healthy, circuit.State);
|
|
Assert.True(circuit.AllowExport());
|
|
}
|
|
|
|
[Fact]
|
|
public void ProbeFailureReopensCircuit()
|
|
{
|
|
var clock = new MutableClock();
|
|
var circuit = new ExternalCircuitBreaker(1, TimeSpan.FromSeconds(30), clock.Now);
|
|
circuit.RecordFailure();
|
|
clock.Advance(TimeSpan.FromSeconds(31));
|
|
Assert.True(circuit.AllowExport()); // half-open sonde
|
|
|
|
circuit.RecordFailure(); // sonde échoue → ré-ouverture
|
|
Assert.Equal(ExternalObservabilityState.Open, circuit.State);
|
|
Assert.False(circuit.AllowExport());
|
|
}
|
|
|
|
[Fact]
|
|
public void NeverAllowsNegativeOrZeroCooldown()
|
|
{
|
|
var clock = new MutableClock();
|
|
var circuit = new ExternalCircuitBreaker(1, TimeSpan.Zero, clock.Now);
|
|
|
|
circuit.RecordFailure();
|
|
Assert.False(circuit.AllowExport()); // cooldown ramené à >= 1 s : encore ouvert
|
|
|
|
clock.Advance(TimeSpan.FromSeconds(2));
|
|
Assert.True(circuit.AllowExport()); // sonde possible après cooldown positif
|
|
}
|
|
|
|
private static ExternalCircuitBreaker NewCircuit() =>
|
|
new(3, TimeSpan.FromSeconds(30));
|
|
|
|
private sealed class MutableClock
|
|
{
|
|
private DateTimeOffset _now = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
|
|
|
public DateTimeOffset Now() => _now;
|
|
|
|
public void Advance(TimeSpan delta) => _now += delta;
|
|
}
|
|
}
|