package config import ( "math" "testing" "gopkg.in/yaml.v3" ) func TestParseSizeUnits(t *testing.T) { cases := []struct { in string want int64 }{ {"0", 0}, {"512", 512}, {"512B", 512}, {"1K", 1 << 10}, {"1KB", 1000}, // decimal, unlike KiB {"1KiB", 1 << 10}, {"1M", 1 << 20}, {"1MiB", 1 << 20}, {"1G", 1 << 30}, {"1GiB", 1 << 30}, {"1T", 1 << 40}, {"1TiB", 1 << 40}, // "M" must not eat the "iB" of "MiB", and "B" must not eat the "iB" // of "BiB"-looking input. These are the shadowing cases the longest // suffix first ordering exists for. {"1MiB", 1 << 20}, {"1MIB", 1 << 20}, {"512mib", 1 << 20 * 512}, {" 2 MiB ", 2 << 20}, // Fractions scale exactly for binary units. {"1.5GiB", 1610612736}, {"0.5KiB", 512}, } for _, tc := range cases { got, err := ParseSize(tc.in) if err != nil { t.Errorf("ParseSize(%q): unexpected error %v", tc.in, err) continue } if int64(got) != tc.want { t.Errorf("ParseSize(%q) = %d, want %d", tc.in, int64(got), tc.want) } } } func TestParseSizeRejectsJunk(t *testing.T) { // Every one of these must be an error rather than a plausible number: // a silently misread memory budget is worse than a refusal to start. for _, in := range []string{ "", " ", "MiB", "abc", "512MiBx", // trailing junk, not a truncated match "1.2.3", "12 34", "-1", "-1MiB", "1MiB-", // negative after the unit "KiB", // a unit with no number in front ".", // a number with no digits } { if got, err := ParseSize(in); err == nil { t.Errorf("ParseSize(%q) = %d, want an error", in, int64(got)) } } } func TestParseSizeRangeAndRounding(t *testing.T) { // A value that does not fit in an int64 is an error, not a wrap. for _, in := range []string{"8EiB", "9223372036854775807B", "99999999999999999999"} { if got, err := ParseSize(in); err == nil { t.Errorf("ParseSize(%q) = %d, want an out-of-range error", in, int64(got)) } } // The largest representable value still parses. if got, err := ParseSize("8EiB"); err == nil && int64(got) <= 0 { t.Errorf("ParseSize(%q) = %d, want a positive value or an error", "8EiB", int64(got)) } // A fractional value truncates rather than rounding up: a memory budget // that rounds up is a budget the process does not have. if got, err := ParseSize("0.1KiB"); err != nil || int64(got) != 102 { t.Errorf("ParseSize(%q) = %d (err %v), want 102", "0.1KiB", int64(got), err) } } // TestParseSizeAcceptsExponents pins that ParseFloat's exponent syntax is // accepted, with or without a unit: the parser is one ParseFloat call in both // paths, and making "1e3" work while "1e3MiB" errors would be a surprise. func TestParseSizeAcceptsExponents(t *testing.T) { if got, err := ParseSize("1e3MiB"); err != nil || int64(got) != 1000*(1<<20) { t.Errorf("ParseSize(%q) = %d (err %v), want %d", "1e3MiB", int64(got), err, 1000*(1<<20)) } } func TestSizeUnmarshalYAML(t *testing.T) { type doc struct { V Size `yaml:"v"` } cases := []struct { in string want int64 }{ {"v: 512MiB\n", 512 << 20}, {"v: \"512MiB\"\n", 512 << 20}, // A bare integer is bytes: that is what MarshalYAML writes, so a // config the API rewrote must load back unchanged. {"v: 1073741824\n", 1 << 30}, {"v: 0\n", 0}, } for _, tc := range cases { var d doc if err := yaml.Unmarshal([]byte(tc.in), &d); err != nil { t.Errorf("unmarshal %q: %v", tc.in, err) continue } if int64(d.V) != tc.want { t.Errorf("unmarshal %q = %d, want %d", tc.in, int64(d.V), tc.want) } } var d doc if err := yaml.Unmarshal([]byte("v: not-a-size\n"), &d); err == nil { t.Error("unmarshal of junk: want an error") } // "1Gi" is the k8s spelling and is *not* in the unit table: the parser is // deliberately strict, so a memory budget must be written 1GiB. Kept as // a test rather than left implicit, because the k8s manifests sit right // next to this code and "1Gi" is what an operator reaches for first. if err := yaml.Unmarshal([]byte("v: 1Gi\n"), &d); err == nil { t.Error(`unmarshal of "1Gi": want an error, the unit is GiB`) } } func TestSizeMarshalYAMLRoundTrips(t *testing.T) { type doc struct { V Size `yaml:"v"` } in := doc{V: Size(1536 << 20)} b, err := yaml.Marshal(in) if err != nil { t.Fatal(err) } var out doc if err := yaml.Unmarshal(b, &out); err != nil { t.Fatalf("round trip: %v", err) } if out.V != in.V { t.Fatalf("round trip = %d, want %d", int64(out.V), int64(in.V)) } } func TestSizeString(t *testing.T) { cases := []struct { in Size want string }{ {0, "0"}, {512, "512B"}, {1 << 10, "1KiB"}, {1536, "1536B"}, // not a whole KiB, so bytes {1 << 20, "1MiB"}, {1 << 30, "1GiB"}, {3<<30 + 7, "3221225479B"}, // 3Gi + 7: no whole unit left, so bytes } for _, tc := range cases { if got := tc.in.String(); got != tc.want { t.Errorf("Size(%d).String() = %q, want %q", int64(tc.in), got, tc.want) } } } func TestMemoryEffectiveLimit(t *testing.T) { if got := (Memory{}).EffectiveLimit(); got != math.MaxInt64 { t.Errorf("no limit: got %d, want MaxInt64 (no ceiling)", got) } // A limit below the reserve still leaves the runtime its floor rather // than a death spiral. if got := (Memory{Limit: 128 << 20}).EffectiveLimit(); got != 64<<20 { t.Errorf("limit below the reserve: got %d, want the 64MiB floor", got) } if got := (Memory{Limit: 64 << 20}).EffectiveLimit(); got != 64<<20 { t.Errorf("limit equal to the floor: got %d, want 64MiB", got) } // The usual case: 1Gi with the default 256MiB reserve. if got := (Memory{Limit: 1 << 30}).EffectiveLimit(); got != (1<<30)-(256<<20) { t.Errorf("1Gi limit: got %d, want %d", got, (1<<30)-(256<<20)) } if got := (Memory{Limit: 1 << 30, Reserve: 300 << 20}).EffectiveLimit(); got != (1<<30)-(300<<20) { t.Errorf("explicit reserve: got %d, want %d", got, (1<<30)-(300<<20)) } } func TestMemoryEffectiveCacheBytes(t *testing.T) { // An explicit budget wins outright: the operator said so. if got := (Memory{Limit: 1 << 30, CacheMaxBytes: 100 << 20}).EffectiveCacheBytes(); got != 100<<20 { t.Errorf("explicit cache budget: got %d, want %d", got, 100<<20) } // Otherwise it is 60% of the effective limit, leaving the rest for the // GC, the live query ring and the per-query working set. m := Memory{Limit: 1 << 30, Reserve: 256 << 20} want := m.EffectiveLimit() * 3 / 5 if got := m.EffectiveCacheBytes(); got != want { t.Errorf("derived cache budget: got %d, want %d", got, want) } // No memory limit at all means no byte budget either, not a made-up one. if got := (Memory{}).EffectiveCacheBytes(); got != 0 { t.Errorf("no limit: got %d, want 0 (unbounded)", got) } }