package host import ( "context" "errors" "strings" "testing" "time" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" ) // TestParsePressureSome60 pins /proc/pressure/memory's shape: two lines, // "some" then "full", each with avg10/avg60/avg300/total. Only some/avg60 // is wanted. A kernel without PSI has no file at all — the caller handles // that — but a file with an unexpected shape reports not-ok rather than 0, // so a parse bug can't masquerade as a healthy box. func TestParsePressureSome60(t *testing.T) { good := "some avg10=0.00 avg60=12.50 avg300=3.10 total=171234\nfull avg10=0.00 avg60=0.00 avg300=0.00 total=153521\n" if got, ok := parsePressureSome60(good); !ok || got != 12.5 { t.Fatalf("parse = (%v, %v), want (12.5, true)", got, ok) } for name, s := range map[string]string{ "empty": "", "full only": "full avg10=0.00 avg60=0.00 avg300=0.00 total=1\n", "no avg60": "some avg10=0.00 avg300=0.00 total=1\n", "not number": "some avg10=0.00 avg60=x avg300=0.00 total=1\n", } { if _, ok := parsePressureSome60(s); ok { t.Errorf("%s: parsed ok, want not-ok", name) } } } // TestMemwatchData: the tick's PutMetricData input. Two data points when // PSI is readable, one when it is not; namespace, dimension and units are // what the region-host module's alarm and IAM condition name — a drift // here is an alarm that never fires, silently. func TestMemwatchData(t *testing.T) { now := time.Date(2026, 8, 28, 11, 4, 0, 0, time.UTC) in := memwatchData("i-0123", 14.2, 30.5, true, now) if *in.Namespace != "Carlos/Host" { t.Fatalf("namespace %q", *in.Namespace) } if len(in.MetricData) != 2 { t.Fatalf("%d data points, want 2", len(in.MetricData)) } names := map[string]float64{} for _, d := range in.MetricData { names[*d.MetricName] = *d.Value if len(d.Dimensions) != 1 || *d.Dimensions[0].Name != "InstanceId" || *d.Dimensions[0].Value != "i-0123" { t.Errorf("%s: dimensions %+v", *d.MetricName, d.Dimensions) } if !d.Timestamp.Equal(now) { t.Errorf("%s: timestamp %v", *d.MetricName, d.Timestamp) } } if names["MemAvailablePercent"] != 14.2 || names["MemoryPressureSome60"] != 30.5 { t.Errorf("values %v", names) } if in := memwatchData("i-0123", 14.2, 0, false, now); len(in.MetricData) != 1 || *in.MetricData[0].MetricName != "MemAvailablePercent" { t.Errorf("without PSI: %+v", in.MetricData) } } type fakePutter struct { calls []*cloudwatch.PutMetricDataInput } func (f *fakePutter) PutMetricData(_ context.Context, in *cloudwatch.PutMetricDataInput, _ ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) { f.calls = append(f.calls, in) return &cloudwatch.PutMetricDataOutput{}, nil } // TestMemwatchTick: one tick, one call, built from the readers it is // handed. An unreadable meminfo is a skipped tick, not a zero. func TestMemwatchTick(t *testing.T) { f := &fakePutter{} w := memwatch{ put: f, instanceID: "i-0123", mem: func() (*int64, *int64) { tot, av := int64(4000), int64(1000); return &tot, &av }, pressure: func() (string, error) { return "some avg10=0 avg60=7.5 avg300=0 total=0\nfull avg10=0 avg60=0 avg300=0 total=0\n", nil }, logf: t.Logf, } w.tick(context.Background(), time.Unix(0, 0)) if len(f.calls) != 1 || len(f.calls[0].MetricData) != 2 { t.Fatalf("calls %+v", f.calls) } if v := *f.calls[0].MetricData[0].Value; v != 25 { t.Errorf("MemAvailablePercent = %v, want 25", v) } w.mem = func() (*int64, *int64) { return nil, nil } w.tick(context.Background(), time.Unix(0, 0)) if len(f.calls) != 1 { t.Errorf("a tick with unreadable meminfo must not put: %d calls", len(f.calls)) } } // TestMemwatchArmed: the gate's three answers. The unknown-value case is // the one that matters — an operator who typed CARLOS_HOST_METRICS=true // gets told what the only value is, in the journal, at arm time. func TestMemwatchArmed(t *testing.T) { for v, want := range map[string]bool{"cloudwatch": true, " cloudwatch ": true, "": false, "true": false} { armed, why := memwatchArmed(func(string) string { return v }) if armed != want { t.Errorf("%q: armed=%v, want %v", v, armed, want) } if !armed && why == "" { t.Errorf("%q: disabled with no reason", v) } } if _, why := memwatchArmed(func(string) string { return "true" }); !strings.Contains(why, `"cloudwatch"`) { t.Errorf("unknown value's reason %q does not name the only valid value", why) } } type failPutter struct{ err error } func (f *failPutter) PutMetricData(context.Context, *cloudwatch.PutMetricDataInput, ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) { return nil, f.err } // TestMemwatchPutFailureLogsOnChange: the same failure minute after minute // is ONE line; a different failure is another; recovery is one more. func TestMemwatchPutFailureLogsOnChange(t *testing.T) { var lines []string fp := &failPutter{err: errors.New("AccessDenied")} w := memwatch{ put: fp, instanceID: "i-0123", mem: func() (*int64, *int64) { tot, av := int64(4000), int64(1000); return &tot, &av }, pressure: func() (string, error) { return "", errors.New("no psi") }, logf: func(f string, a ...any) { lines = append(lines, f) }, } for i := 0; i < 3; i++ { w.tick(context.Background(), time.Unix(0, 0)) } if len(lines) != 1 { t.Fatalf("3 identical failures logged %d lines, want 1: %v", len(lines), lines) } fp.err = errors.New("Throttling") w.tick(context.Background(), time.Unix(0, 0)) if len(lines) != 2 { t.Fatalf("a changed failure did not log: %v", lines) } w.put = &fakePutter{} w.tick(context.Background(), time.Unix(0, 0)) w.tick(context.Background(), time.Unix(0, 0)) if len(lines) != 3 || !strings.Contains(lines[2], "recovered") { t.Fatalf("recovery logged wrong: %v", lines) } }