Skip to content
quarry Artifact [39db7264d4]
Login

Artifact [39db7264d4]

Artifact 39db7264d4a5faf4a49c77c7211c70242b605d4311f9863baf788e93bdfacc34:


package obs

import (
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"
)

const statusFixture = `Name:	quarry
VmRSS:	  9000000 kB
RssAnon:	  6291456 kB
RssFile:	   204800 kB
RssShmem:	       0 kB
`

const meminfoFixture = `MemTotal:       16000000 kB
MemFree:          500000 kB
MemAvailable:    2097152 kB
SwapTotal:       4194304 kB
SwapFree:         419430 kB
`

func TestParseProc(t *testing.T) {
	a, f := parseStatus(statusFixture)
	if a != 6<<30 || f != 200<<20 {
		t.Errorf("FAIL: status anon=%d file=%d", a, f)
	}
	a, f = parseStatus("VmRSS:\t  1024 kB\n")
	if a != 1<<20 || f != 0 {
		t.Errorf("FAIL: VmRSS fallback anon=%d file=%d", a, f)
	}
	av, sf, st := parseMeminfo(meminfoFixture)
	if av != 2<<30 || sf != 419430<<10 || st != 4<<30 {
		t.Errorf("FAIL: meminfo %d %d %d", av, sf, st)
	}
	if a, f := parseStatus("garbage\nRssAnon: x kB\n"); a != 0 || f != 0 {
		t.Errorf("FAIL: garbage read as %d %d", a, f)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// With no /proc every reading is zero and nothing warns.
func TestNoProcNoWarning(t *testing.T) {
	old := procRead
	defer func() { procRead = old }()
	procRead = func(string) ([]byte, error) { return nil, errors.New("no /proc") }
	if p := readProcMem(); p != (procMem{}) {
		t.Errorf("FAIL: readings without /proc: %+v", p)
	}
	s := readMemory()
	if s.Anon != 0 || s.Available != 0 || s.SwapTotal != 0 {
		t.Errorf("FAIL: sample without /proc: %+v", s)
	}
	if w := Warnings([]MemSample{s, s}, 0); len(w) != 0 {
		t.Errorf("FAIL: warnings from unknowns: %+v", w)
	}
	if w := Warnings([]MemSample{s}, 9<<30); len(w) != 0 {
		t.Errorf("FAIL: cgroup warning from unknowns: %+v", w)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

func TestReadProcMemInjected(t *testing.T) {
	old := procRead
	defer func() { procRead = old }()
	procRead = func(p string) ([]byte, error) {
		switch p {
		case "/proc/self/status":
			return []byte(statusFixture), nil
		case "/proc/meminfo":
			return []byte(meminfoFixture), nil
		}
		return nil, errors.New("absent")
	}
	p := readProcMem()
	if p.Anon != 6<<30 || p.Available != 2<<30 || p.SwapTotal != 4<<30 {
		t.Errorf("FAIL: %+v", p)
	}
}

func kinds(ws []MemWarning) string {
	var k []string
	for _, w := range ws {
		k = append(k, w.Kind)
	}
	return strings.Join(k, ",")
}

// series builds one sample a minute ending at end, anon from f(minutes ago).
func series(end time.Time, mins int, f func(ago int) uint64) []MemSample {
	var s []MemSample
	for ago := mins; ago >= 0; ago-- {
		s = append(s, MemSample{At: end.Add(-time.Duration(ago) * time.Minute), Anon: f(ago)})
	}
	return s
}

func TestWarnAnonWindow(t *testing.T) {
	end := time.Now()
	over := func(ago int) uint64 { return WarnAnon }
	if w := Warnings(series(end, 10, over), 0); kinds(w) != WarnKindAnon {
		t.Errorf("FAIL: at the line for 10 min: %q", kinds(w))
	}
	if w := Warnings(series(end, 9, over), 0); len(w) != 0 {
		t.Errorf("FAIL: at the line for 9 min: %q", kinds(w))
	}
	below := func(ago int) uint64 { return WarnAnon - 1 }
	if w := Warnings(series(end, 30, below), 0); len(w) != 0 {
		t.Errorf("FAIL: one byte under: %q", kinds(w))
	}
	// A dip inside the window resets it.
	dip := func(ago int) uint64 {
		if ago == 5 {
			return 1
		}
		return WarnAnon
	}
	if w := Warnings(series(end, 12, dip), 0); len(w) != 0 {
		t.Errorf("FAIL: a dip 5 min ago still warned: %q", kinds(w))
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

func TestWarnGrowthWindow(t *testing.T) {
	end := time.Now()
	// anon rises perHour bytes each hour, evenly, for mins minutes.
	rise := func(perHour uint64) func(int) uint64 {
		return func(ago int) uint64 { return 1<<30 + (1000-uint64(ago))*perHour/60 }
	}
	if w := Warnings(series(end, 180, rise(WarnGrowthPerHour)), 0); kinds(w) != WarnKindGrowth {
		t.Errorf("FAIL: 300 MiB/h for 3 h: %q", kinds(w))
	}
	if w := Warnings(series(end, 180, rise(WarnGrowthPerHour-120)), 0); len(w) != 0 {
		t.Errorf("FAIL: just under the rate: %q", kinds(w))
	}
	if w := Warnings(series(end, 150, rise(WarnGrowthPerHour)), 0); len(w) != 0 {
		t.Errorf("FAIL: only 2.5 h of readings: %q", kinds(w))
	}
	// Fast for two hours, then flat: not sustained.
	flat := func(ago int) uint64 {
		if ago < 60 {
			return rise(WarnGrowthPerHour)(60)
		}
		return rise(WarnGrowthPerHour)(ago)
	}
	if w := Warnings(series(end, 180, flat), 0); len(w) != 0 {
		t.Errorf("FAIL: last hour flat still warned: %q", kinds(w))
	}
	// The day-long ring covers the window.
	if MemoryKept < WarnGrowthFor {
		t.Errorf("FAIL: ring keeps %s, rule needs %s", MemoryKept, WarnGrowthFor)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

func TestWarnHostSwapCgroup(t *testing.T) {
	now := time.Now()
	one := func(s MemSample, cap uint64) string { s.At = now; return kinds(Warnings([]MemSample{s}, cap)) }
	if k := one(MemSample{Available: WarnHostAvailable}, 0); k != "" {
		t.Errorf("FAIL: available at the line: %q", k)
	}
	if k := one(MemSample{Available: WarnHostAvailable - 1}, 0); k != WarnKindHost {
		t.Errorf("FAIL: available under the line: %q", k)
	}
	total := uint64(1000 << 20)
	if k := one(MemSample{SwapTotal: total, SwapFree: total / 10 * 2}, 0); k != "" {
		t.Errorf("FAIL: swap 80%% used: %q", k)
	}
	if k := one(MemSample{SwapTotal: total, SwapFree: total / 10}, 0); k != WarnKindSwap {
		t.Errorf("FAIL: swap 90%% used: %q", k)
	}
	if k := one(MemSample{SwapTotal: 0, SwapFree: 0}, 0); k != "" {
		t.Errorf("FAIL: no swap configured: %q", k)
	}
	capb := uint64(10 << 30)
	if k := one(MemSample{Anon: capb/10*8 - 1}, capb); k != "" {
		t.Errorf("FAIL: under 80%% of the cap: %q", k)
	}
	if k := one(MemSample{Anon: capb / 10 * 8}, capb); k != WarnKindCgroup {
		t.Errorf("FAIL: at 80%% of the cap: %q", k)
	}
	if k := one(MemSample{Anon: capb}, 0); k != "" {
		t.Errorf("FAIL: no cap set: %q", k)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// One line when a warning starts, one when it clears, none between.
func TestWatchLogsEdges(t *testing.T) {
	var lines []string
	w := memWatch{logf: func(f string, a ...any) { lines = append(lines, fmt.Sprintf(f, a...)) }}
	now := time.Now()
	on := []MemWarning{{Kind: WarnKindHost, Value: 1, Threshold: 2}}
	w.observe(now, nil)
	w.observe(now.Add(time.Minute), on)
	w.observe(now.Add(2*time.Minute), on)
	w.observe(now.Add(3*time.Minute), on)
	if len(lines) != 1 || !strings.Contains(lines[0], WarnKindHost) {
		t.Fatalf("FAIL: want one start line, got %q", lines)
	}
	w.observe(now.Add(4*time.Minute), nil)
	w.observe(now.Add(5*time.Minute), nil)
	if len(lines) != 2 || !strings.Contains(lines[1], WarnKindHost) {
		t.Fatalf("FAIL: want a clear line, got %q", lines)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

func TestMemoryNowCarriesWarningsField(t *testing.T) {
	if m := MemoryNow(); m.Warnings == nil {
		t.Error("FAIL: Warnings is nil, which marshals as null")
	}
}