package api
import (
"encoding/json"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/obs"
)
// The memory verdict: quiet under the target, a finding over it that
// says by how much, for how long and how much is unexplained, and a
// warning for heavy swap alone.
func TestMemoryFindings(t *testing.T) {
const gib = 1 << 30
quiet := opsMemory{Memory: obs.Memory{Heap: 2 * gib, Limit: 7 * gib, Cap: 9 * gib}}
if f := memoryFindings(quiet); len(f) != 0 {
t.Fatalf("FAIL: a heap under its target is a finding: %+v", f)
}
over := opsMemory{Memory: obs.Memory{Heap: 10 * gib, Limit: 7 * gib, Cap: 9 * gib, Swap: 3 * gib,
OverSince: time.Now().Add(-6 * time.Hour)}, Unaccounted: 9 * gib}
f := memoryFindings(over)
if len(f) != 1 || f[0].Severity != "bad" || f[0].About != "memory" {
t.Fatalf("FAIL: a heap over its target: %+v", f)
}
for _, want := range []string{"1.4×", "6h", "3.0 GB swapped", "9.0 GB of it not accounted"} {
if !strings.Contains(f[0].Text, want) {
t.Errorf("FAIL: the finding does not say %q: %s", want, f[0].Text)
}
}
nearCap := opsMemory{Memory: obs.Memory{Heap: 8_800 << 20, Cap: 9 * gib}}
if f := memoryFindings(nearCap); len(f) != 1 || f[0].Severity != "bad" {
t.Fatalf("FAIL: a heap at 95%% of the cap with no target set: %+v", f)
}
swapped := opsMemory{Memory: obs.Memory{Heap: 2 * gib, Limit: 7 * gib, Swap: 2 * gib}}
if f := memoryFindings(swapped); len(f) != 1 || f[0].Severity != "warn" {
t.Fatalf("FAIL: heavy swap under the target: %+v", f)
}
t.Log("ok: quiet, over target, near cap, swap")
}
// Each warning kind becomes its own finding, keyed by kind, and the
// payload carries the data fields.
func TestMemoryWarningsAreFindings(t *testing.T) {
m := opsMemory{Memory: obs.Memory{Heap: 1 << 30, Warnings: []obs.MemWarning{
{Kind: obs.WarnKindAnon, Value: 7 << 30, Threshold: obs.WarnAnon, Since: time.Now().Add(-20 * time.Minute)},
{Kind: obs.WarnKindGrowth, Value: 400 << 20, Threshold: obs.WarnGrowthPerHour},
{Kind: obs.WarnKindHost, Value: 1 << 30, Threshold: obs.WarnHostAvailable},
{Kind: obs.WarnKindSwap, Value: 950, Threshold: 900},
{Kind: obs.WarnKindCgroup, Value: 8 << 30, Threshold: 8 << 30},
}}}
got := map[string]string{}
for _, f := range memoryFindings(m) {
got[f.Key] = f.Severity
if f.About != "memory" || f.Figure == "" {
t.Errorf("FAIL: finding %s: about %q figure %q", f.Key, f.About, f.Figure)
}
}
for _, k := range []string{obs.WarnKindAnon, obs.WarnKindGrowth, obs.WarnKindHost, obs.WarnKindSwap, obs.WarnKindCgroup} {
if got["memory:"+k] == "" {
t.Errorf("FAIL: no finding for kind %s: %v", k, got)
}
}
if got["memory:"+obs.WarnKindGrowth] != "warn" {
t.Errorf("FAIL: growth should be a warn: %v", got)
}
if f := memoryFindings(opsMemory{}); len(f) != 0 {
t.Errorf("FAIL: no warnings, got findings %+v", f)
}
// Wire: the status payload has the literal keys.
b, err := json.Marshal(opsMemoryNow())
if err != nil {
t.Fatal(err)
}
var raw map[string]json.RawMessage
if err := json.Unmarshal(b, &raw); err != nil {
t.Fatal(err)
}
if _, ok := raw["warnings"]; !ok {
t.Errorf("FAIL: ops.status memory has no warnings key: %s", b)
}
if !t.Failed() {
t.Log("ok")
}
}