carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/aws/aws-sdk-go-v2/service/cloudwatch"
11)
12
13// TestParsePressureSome60 pins /proc/pressure/memory's shape: two lines,
14// "some" then "full", each with avg10/avg60/avg300/total. Only some/avg60
15// is wanted. A kernel without PSI has no file at all — the caller handles
16// that — but a file with an unexpected shape reports not-ok rather than 0,
17// so a parse bug can't masquerade as a healthy box.
18func TestParsePressureSome60(t *testing.T) {
19 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"
20 if got, ok := parsePressureSome60(good); !ok || got != 12.5 {
21 t.Fatalf("parse = (%v, %v), want (12.5, true)", got, ok)
22 }
23 for name, s := range map[string]string{
24 "empty": "",
25 "full only": "full avg10=0.00 avg60=0.00 avg300=0.00 total=1\n",
26 "no avg60": "some avg10=0.00 avg300=0.00 total=1\n",
27 "not number": "some avg10=0.00 avg60=x avg300=0.00 total=1\n",
28 } {
29 if _, ok := parsePressureSome60(s); ok {
30 t.Errorf("%s: parsed ok, want not-ok", name)
31 }
32 }
33}
34
35// TestMemwatchData: the tick's PutMetricData input. Two data points when
36// PSI is readable, one when it is not; namespace, dimension and units are
37// what the region-host module's alarm and IAM condition name — a drift
38// here is an alarm that never fires, silently.
39func TestMemwatchData(t *testing.T) {
40 now := time.Date(2026, 8, 28, 11, 4, 0, 0, time.UTC)
41 in := memwatchData("i-0123", 14.2, 30.5, true, now)
42 if *in.Namespace != "Carlos/Host" {
43 t.Fatalf("namespace %q", *in.Namespace)
44 }
45 if len(in.MetricData) != 2 {
46 t.Fatalf("%d data points, want 2", len(in.MetricData))
47 }
48 names := map[string]float64{}
49 for _, d := range in.MetricData {
50 names[*d.MetricName] = *d.Value
51 if len(d.Dimensions) != 1 || *d.Dimensions[0].Name != "InstanceId" || *d.Dimensions[0].Value != "i-0123" {
52 t.Errorf("%s: dimensions %+v", *d.MetricName, d.Dimensions)
53 }
54 if !d.Timestamp.Equal(now) {
55 t.Errorf("%s: timestamp %v", *d.MetricName, d.Timestamp)
56 }
57 }
58 if names["MemAvailablePercent"] != 14.2 || names["MemoryPressureSome60"] != 30.5 {
59 t.Errorf("values %v", names)
60 }
61 if in := memwatchData("i-0123", 14.2, 0, false, now); len(in.MetricData) != 1 || *in.MetricData[0].MetricName != "MemAvailablePercent" {
62 t.Errorf("without PSI: %+v", in.MetricData)
63 }
64}
65
66type fakePutter struct {
67 calls []*cloudwatch.PutMetricDataInput
68}
69
70func (f *fakePutter) PutMetricData(_ context.Context, in *cloudwatch.PutMetricDataInput, _ ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) {
71 f.calls = append(f.calls, in)
72 return &cloudwatch.PutMetricDataOutput{}, nil
73}
74
75// TestMemwatchTick: one tick, one call, built from the readers it is
76// handed. An unreadable meminfo is a skipped tick, not a zero.
77func TestMemwatchTick(t *testing.T) {
78 f := &fakePutter{}
79 w := memwatch{
80 put: f,
81 instanceID: "i-0123",
82 mem: func() (*int64, *int64) { tot, av := int64(4000), int64(1000); return &tot, &av },
83 pressure: func() (string, error) {
84 return "some avg10=0 avg60=7.5 avg300=0 total=0\nfull avg10=0 avg60=0 avg300=0 total=0\n", nil
85 },
86 logf: t.Logf,
87 }
88 w.tick(context.Background(), time.Unix(0, 0))
89 if len(f.calls) != 1 || len(f.calls[0].MetricData) != 2 {
90 t.Fatalf("calls %+v", f.calls)
91 }
92 if v := *f.calls[0].MetricData[0].Value; v != 25 {
93 t.Errorf("MemAvailablePercent = %v, want 25", v)
94 }
95 w.mem = func() (*int64, *int64) { return nil, nil }
96 w.tick(context.Background(), time.Unix(0, 0))
97 if len(f.calls) != 1 {
98 t.Errorf("a tick with unreadable meminfo must not put: %d calls", len(f.calls))
99 }
100}
101
102// TestMemwatchArmed: the gate's three answers. The unknown-value case is
103// the one that matters — an operator who typed CARLOS_HOST_METRICS=true
104// gets told what the only value is, in the journal, at arm time.
105func TestMemwatchArmed(t *testing.T) {
106 for v, want := range map[string]bool{"cloudwatch": true, " cloudwatch ": true, "": false, "true": false} {
107 armed, why := memwatchArmed(func(string) string { return v })
108 if armed != want {
109 t.Errorf("%q: armed=%v, want %v", v, armed, want)
110 }
111 if !armed && why == "" {
112 t.Errorf("%q: disabled with no reason", v)
113 }
114 }
115 if _, why := memwatchArmed(func(string) string { return "true" }); !strings.Contains(why, `"cloudwatch"`) {
116 t.Errorf("unknown value's reason %q does not name the only valid value", why)
117 }
118}
119
120type failPutter struct{ err error }
121
122func (f *failPutter) PutMetricData(context.Context, *cloudwatch.PutMetricDataInput, ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) {
123 return nil, f.err
124}
125
126// TestMemwatchPutFailureLogsOnChange: the same failure minute after minute
127// is ONE line; a different failure is another; recovery is one more.
128func TestMemwatchPutFailureLogsOnChange(t *testing.T) {
129 var lines []string
130 fp := &failPutter{err: errors.New("AccessDenied")}
131 w := memwatch{
132 put: fp,
133 instanceID: "i-0123",
134 mem: func() (*int64, *int64) { tot, av := int64(4000), int64(1000); return &tot, &av },
135 pressure: func() (string, error) { return "", errors.New("no psi") },
136 logf: func(f string, a ...any) { lines = append(lines, f) },
137 }
138 for i := 0; i < 3; i++ {
139 w.tick(context.Background(), time.Unix(0, 0))
140 }
141 if len(lines) != 1 {
142 t.Fatalf("3 identical failures logged %d lines, want 1: %v", len(lines), lines)
143 }
144 fp.err = errors.New("Throttling")
145 w.tick(context.Background(), time.Unix(0, 0))
146 if len(lines) != 2 {
147 t.Fatalf("a changed failure did not log: %v", lines)
148 }
149 w.put = &fakePutter{}
150 w.tick(context.Background(), time.Unix(0, 0))
151 w.tick(context.Background(), time.Unix(0, 0))
152 if len(lines) != 3 || !strings.Contains(lines[2], "recovered") {
153 t.Fatalf("recovery logged wrong: %v", lines)
154 }
155}
156