mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-24 20:06:21 +02:00
184 lines
4.5 KiB
Go
184 lines
4.5 KiB
Go
// Copyright 2026 Syntrex Lab. All rights reserved.
|
|
// Use of this source code is governed by an Apache-2.0 license
|
|
// that can be found in the LICENSE file.
|
|
|
|
package circuitbreaker
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNew_DefaultState(t *testing.T) {
|
|
b := New(nil)
|
|
assert.Equal(t, StateHealthy, b.CurrentState())
|
|
assert.True(t, b.IsAllowed())
|
|
}
|
|
|
|
func TestBreaker_HealthyToDegraded(t *testing.T) {
|
|
b := New(&Config{DegradeThreshold: 3})
|
|
|
|
// 2 anomalies: still healthy.
|
|
b.RecordAnomaly("test1")
|
|
b.RecordAnomaly("test2")
|
|
assert.Equal(t, StateHealthy, b.CurrentState())
|
|
|
|
// 3rd anomaly: degrade.
|
|
b.RecordAnomaly("test3")
|
|
assert.Equal(t, StateDegraded, b.CurrentState())
|
|
assert.True(t, b.IsAllowed(), "degraded still allows pipeline")
|
|
}
|
|
|
|
func TestBreaker_DegradedToOpen(t *testing.T) {
|
|
b := New(&Config{DegradeThreshold: 1, OpenThreshold: 3})
|
|
|
|
// Trigger degraded.
|
|
halted := b.RecordAnomaly("trigger degrade")
|
|
assert.False(t, halted)
|
|
assert.Equal(t, StateDegraded, b.CurrentState())
|
|
|
|
// More anomalies until open.
|
|
b.RecordAnomaly("a2")
|
|
halted = b.RecordAnomaly("a3")
|
|
assert.True(t, halted)
|
|
assert.Equal(t, StateOpen, b.CurrentState())
|
|
assert.False(t, b.IsAllowed())
|
|
}
|
|
|
|
func TestBreaker_DegradedRecovery(t *testing.T) {
|
|
b := New(&Config{DegradeThreshold: 1, RecoveryThreshold: 2})
|
|
|
|
b.RecordAnomaly("trigger")
|
|
assert.Equal(t, StateDegraded, b.CurrentState())
|
|
|
|
// 1 clean: not enough.
|
|
b.RecordClean()
|
|
assert.Equal(t, StateDegraded, b.CurrentState())
|
|
|
|
// 2 clean: recovery.
|
|
b.RecordClean()
|
|
assert.Equal(t, StateHealthy, b.CurrentState())
|
|
}
|
|
|
|
func TestBreaker_RecoveryResetByAnomaly(t *testing.T) {
|
|
b := New(&Config{DegradeThreshold: 1, RecoveryThreshold: 3})
|
|
|
|
b.RecordAnomaly("trigger")
|
|
b.RecordClean()
|
|
b.RecordClean()
|
|
// Anomaly resets consecutive clean count.
|
|
b.RecordAnomaly("reset")
|
|
b.RecordClean()
|
|
assert.Equal(t, StateDegraded, b.CurrentState(), "recovery should be reset")
|
|
}
|
|
|
|
func TestBreaker_ManualReset(t *testing.T) {
|
|
b := New(&Config{DegradeThreshold: 1, OpenThreshold: 2})
|
|
|
|
b.RecordAnomaly("a1")
|
|
b.RecordAnomaly("a2")
|
|
assert.Equal(t, StateOpen, b.CurrentState())
|
|
|
|
b.Reset("external watchdog")
|
|
assert.Equal(t, StateHealthy, b.CurrentState())
|
|
assert.True(t, b.IsAllowed())
|
|
}
|
|
|
|
func TestBreaker_WatchdogAutoReset(t *testing.T) {
|
|
b := New(&Config{
|
|
DegradeThreshold: 1,
|
|
OpenThreshold: 2,
|
|
WatchdogTimeout: 10 * time.Millisecond,
|
|
})
|
|
|
|
b.RecordAnomaly("a1")
|
|
b.RecordAnomaly("a2")
|
|
assert.Equal(t, StateOpen, b.CurrentState())
|
|
|
|
// Wait for watchdog.
|
|
time.Sleep(15 * time.Millisecond)
|
|
|
|
// RecordClean triggers watchdog check.
|
|
state := b.RecordClean()
|
|
assert.Equal(t, StateHealthy, state)
|
|
}
|
|
|
|
func TestBreaker_DegradedReducesIterations(t *testing.T) {
|
|
b := New(&Config{DegradeThreshold: 1, DegradedMaxIterations: 2})
|
|
|
|
assert.Equal(t, 5, b.MaxIterations(5), "healthy: full iterations")
|
|
|
|
b.RecordAnomaly("trigger")
|
|
assert.Equal(t, 2, b.MaxIterations(5), "degraded: reduced iterations")
|
|
}
|
|
|
|
func TestBreaker_Events(t *testing.T) {
|
|
b := New(&Config{DegradeThreshold: 1, OpenThreshold: 2})
|
|
|
|
b.RecordAnomaly("a1")
|
|
b.RecordAnomaly("a2")
|
|
b.Reset("test")
|
|
|
|
events := b.Events()
|
|
require.Len(t, events, 3) // HEALTHY→DEGRADED, DEGRADED→OPEN, OPEN→HEALTHY
|
|
assert.Equal(t, StateHealthy, events[0].From)
|
|
assert.Equal(t, StateDegraded, events[0].To)
|
|
assert.Equal(t, StateDegraded, events[1].From)
|
|
assert.Equal(t, StateOpen, events[1].To)
|
|
assert.Equal(t, StateOpen, events[2].From)
|
|
assert.Equal(t, StateHealthy, events[2].To)
|
|
}
|
|
|
|
func TestBreaker_GetStatus(t *testing.T) {
|
|
b := New(nil)
|
|
b.RecordAnomaly("test")
|
|
|
|
s := b.GetStatus()
|
|
assert.Equal(t, "HEALTHY", s.State)
|
|
assert.Equal(t, 1, s.AnomalyCount)
|
|
assert.Equal(t, 1, s.TotalAnomalies)
|
|
assert.GreaterOrEqual(t, s.UptimeSeconds, 0.0)
|
|
}
|
|
|
|
func TestBreaker_StateString(t *testing.T) {
|
|
assert.Equal(t, "HEALTHY", StateHealthy.String())
|
|
assert.Equal(t, "DEGRADED", StateDegraded.String())
|
|
assert.Equal(t, "OPEN", StateOpen.String())
|
|
assert.Equal(t, "UNKNOWN", State(99).String())
|
|
}
|
|
|
|
func TestBreaker_ConcurrentSafety(t *testing.T) {
|
|
b := New(&Config{DegradeThreshold: 100})
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
for i := 0; i < 50; i++ {
|
|
b.RecordAnomaly("concurrent")
|
|
}
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
go func() {
|
|
for i := 0; i < 50; i++ {
|
|
b.RecordClean()
|
|
}
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
go func() {
|
|
for i := 0; i < 50; i++ {
|
|
_ = b.GetStatus()
|
|
_ = b.IsAllowed()
|
|
_ = b.CurrentState()
|
|
}
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
<-done
|
|
<-done
|
|
<-done
|
|
// No race condition panic = pass.
|
|
}
|