Files
felhom-controller/controller/cmd/controller/d28_unhealthy_stop_test.go
T

91 lines
3.4 KiB
Go

package main
import (
"go/ast"
"io"
"log"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// `09` §3 decision 28 (v0.269.0) — the box stops a crash loop / OOM storm; never inside a backup's,
// a quiesce's or an update's own stop, never twice, never an app already held.
func TestD28_StopHoldTellInOrder_AndTheSkips(t *testing.T) {
var calls []string
d := unhealthyDeps{
verdicts: func(time.Time, []stacks.OOMContainer) []stacks.UnhealthyVerdict {
return []stacks.UnhealthyVerdict{
{Stack: "gokapi", Kind: stacks.UnhealthyCrashLoop, Count: 7, Window: 10 * time.Minute},
{Stack: "quiesced", Kind: stacks.UnhealthyCrashLoop, Count: 7},
{Stack: "busy", Kind: stacks.UnhealthyCrashLoop, Count: 7},
{Stack: "held", Kind: stacks.UnhealthyOOMStorm, Count: 20},
}
},
suppressed: func() map[string]bool { return map[string]bool{"quiesced": true} },
busy: func(n string) (bool, string) { return n == "busy", "a restore is running" },
holdKind: func(n string) string {
if n == "held" {
return "update_failed"
}
return ""
},
stop: func(n string) error { calls = append(calls, "stop:"+n); return nil },
hold: func(n, k string, _ time.Time) (int, error) { calls = append(calls, "hold:"+n+":"+k); return 1, nil },
notify: func(v stacks.UnhealthyVerdict, trip int) { calls = append(calls, "notify:"+v.Stack) },
}
stopped := stopUnhealthyApps(log.New(io.Discard, "", 0), d, nil, time.Now())
if strings.Join(calls, ",") != "stop:gokapi,hold:gokapi:crash_loop,notify:gokapi" {
t.Fatalf("calls = %v — want gokapi stopped, held, told; the quiesced, busy and held apps untouched", calls)
}
if len(stopped) != 1 || stopped[0] != "gokapi" {
t.Fatalf("stopped = %v", stopped)
}
}
// A failed stop is neither held nor announced (the household would read "we stopped it" over a running app).
func TestD28_AFailedStopIsNotAnnounced(t *testing.T) {
var calls []string
d := unhealthyDeps{
verdicts: func(time.Time, []stacks.OOMContainer) []stacks.UnhealthyVerdict {
return []stacks.UnhealthyVerdict{{Stack: "x", Kind: "crash_loop"}}
},
suppressed: func() map[string]bool { return nil },
busy: func(string) (bool, string) { return false, "" },
holdKind: func(string) string { return "" },
stop: func(string) error { return io.ErrUnexpectedEOF },
hold: func(string, string, time.Time) (int, error) { calls = append(calls, "hold"); return 1, nil },
notify: func(stacks.UnhealthyVerdict, int) { calls = append(calls, "notify") },
}
stopUnhealthyApps(log.New(io.Discard, "", 0), d, nil, time.Now())
if len(calls) != 0 {
t.Fatalf("calls = %v after a failed stop", calls)
}
}
// The wiring: main's dead-app loop calls stopUnhealthyApps with the detector.
func TestD28_TheLoopStopsUnhealthyApps(t *testing.T) {
found, withDetector := false, false
ast.Inspect(mainBody(t), func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
if id, ok := call.Fun.(*ast.Ident); ok && id.Name == "stopUnhealthyApps" {
found = true
ast.Inspect(call, func(m ast.Node) bool {
if sel, ok := m.(*ast.SelectorExpr); ok && sel.Sel.Name == "ObserveUnhealthy" {
withDetector = true
}
return true
})
}
return true
})
if !found || !withDetector {
t.Fatalf("main calls stopUnhealthyApps=%v with ObserveUnhealthy=%v — decision 28 is built and never run", found, withDetector)
}
}