112 lines
3.5 KiB
Go
112 lines
3.5 KiB
Go
package checks
|
|
|
|
import (
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jasper/quptime/internal/config"
|
|
)
|
|
|
|
func TestAggregatorHysteresisRequiresConsecutiveEvals(t *testing.T) {
|
|
cluster := &config.ClusterConfig{Checks: []config.Check{
|
|
{ID: "c1", Name: "x", Interval: 10 * time.Second},
|
|
}}
|
|
|
|
var transitions atomic.Int32
|
|
agg := NewAggregator(cluster, func(_ *config.Check, _, _ State, _ Snapshot) {
|
|
transitions.Add(1)
|
|
})
|
|
|
|
// First OK submission — candidate=Up, committed still Unknown.
|
|
agg.Submit("nodeA", Result{CheckID: "c1", OK: true, Timestamp: time.Now()})
|
|
snap, _ := agg.SnapshotFor("c1")
|
|
if snap.State != StateUnknown {
|
|
t.Errorf("after one tick state=%s want unknown", snap.State)
|
|
}
|
|
if transitions.Load() != 0 {
|
|
t.Errorf("transitions=%d after one tick, want 0", transitions.Load())
|
|
}
|
|
|
|
// Second OK — hysteresis satisfied, commit Up.
|
|
agg.Submit("nodeA", Result{CheckID: "c1", OK: true, Timestamp: time.Now()})
|
|
snap, _ = agg.SnapshotFor("c1")
|
|
if snap.State != StateUp {
|
|
t.Errorf("after two ticks state=%s want up", snap.State)
|
|
}
|
|
if transitions.Load() != 1 {
|
|
t.Errorf("transitions=%d after commit, want 1", transitions.Load())
|
|
}
|
|
|
|
// Single failure — candidate flips to Down, committed stays Up.
|
|
agg.Submit("nodeA", Result{CheckID: "c1", OK: false, Detail: "boom", Timestamp: time.Now()})
|
|
snap, _ = agg.SnapshotFor("c1")
|
|
if snap.State != StateUp {
|
|
t.Errorf("single fail flipped state prematurely: %s", snap.State)
|
|
}
|
|
|
|
// Second failure — commit Down.
|
|
agg.Submit("nodeA", Result{CheckID: "c1", OK: false, Detail: "boom", Timestamp: time.Now()})
|
|
snap, _ = agg.SnapshotFor("c1")
|
|
if snap.State != StateDown {
|
|
t.Errorf("after two fails state=%s want down", snap.State)
|
|
}
|
|
if transitions.Load() != 2 {
|
|
t.Errorf("transitions=%d after second commit, want 2", transitions.Load())
|
|
}
|
|
}
|
|
|
|
func TestAggregatorMajorityRule(t *testing.T) {
|
|
cluster := &config.ClusterConfig{Checks: []config.Check{
|
|
{ID: "c1", Name: "x", Interval: 10 * time.Second},
|
|
}}
|
|
agg := NewAggregator(cluster, nil)
|
|
|
|
// 2 OK + 1 fail → candidate Up.
|
|
now := time.Now()
|
|
agg.Submit("a", Result{CheckID: "c1", OK: true, Timestamp: now})
|
|
agg.Submit("b", Result{CheckID: "c1", OK: true, Timestamp: now})
|
|
agg.Submit("c", Result{CheckID: "c1", OK: false, Timestamp: now})
|
|
|
|
snap, _ := agg.SnapshotFor("c1")
|
|
if snap.OKCount != 2 || snap.NotOK != 1 {
|
|
t.Errorf("counts wrong: %+v", snap)
|
|
}
|
|
|
|
// flip the majority
|
|
for i := 0; i < 2; i++ {
|
|
agg.Submit("a", Result{CheckID: "c1", OK: false, Timestamp: time.Now()})
|
|
agg.Submit("b", Result{CheckID: "c1", OK: false, Timestamp: time.Now()})
|
|
agg.Submit("c", Result{CheckID: "c1", OK: false, Timestamp: time.Now()})
|
|
}
|
|
snap, _ = agg.SnapshotFor("c1")
|
|
if snap.State != StateDown {
|
|
t.Errorf("majority-fail did not transition to down: %s", snap.State)
|
|
}
|
|
}
|
|
|
|
func TestAggregatorDropsUnknownChecks(t *testing.T) {
|
|
cluster := &config.ClusterConfig{}
|
|
agg := NewAggregator(cluster, nil)
|
|
|
|
agg.Submit("a", Result{CheckID: "ghost", OK: true, Timestamp: time.Now()})
|
|
if _, ok := agg.SnapshotFor("ghost"); ok {
|
|
t.Error("aggregator kept state for unconfigured check")
|
|
}
|
|
}
|
|
|
|
func TestAggregatorIgnoresStaleResults(t *testing.T) {
|
|
cluster := &config.ClusterConfig{Checks: []config.Check{
|
|
{ID: "c1", Name: "x", Interval: 10 * time.Second},
|
|
}}
|
|
agg := NewAggregator(cluster, nil)
|
|
|
|
old := time.Now().Add(-10 * time.Minute)
|
|
agg.Submit("a", Result{CheckID: "c1", OK: true, Timestamp: old})
|
|
|
|
snap, _ := agg.SnapshotFor("c1")
|
|
if snap.Reports != 0 {
|
|
t.Errorf("stale report counted: %+v", snap)
|
|
}
|
|
}
|