package config import ( "errors" "io" "log/slog" "os" "path/filepath" "strings" "testing" ) // discardLogger returns a logger that swallows all output, for tests // that exercise code paths which log. func discardLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } // TestCacheMaxBytesExplicitValueUsedWithoutFloor verifies that an // explicitly configured cache_max_bytes value is used exactly as // given: the 500 MiB floor applies only to the computed default, never // to explicit values. func TestCacheMaxBytesExplicitValueUsedWithoutFloor(t *testing.T) { yamlContent := "signing_key: " + validTestSigningKey + "\ncache_max_bytes: 1024\n" c, err := configFromYAML(t, yamlContent) if err != nil { t.Fatalf("explicit cache_max_bytes must be accepted, got error: %v", err) } if c.CacheMaxBytes != 1024 { t.Errorf("CacheMaxBytes = %d, want 1024 (no floor for explicit values)", c.CacheMaxBytes) } } // TestCacheMaxBytesZeroIsValidAndDisablesCache verifies that an // explicit zero is a valid value (it disables the disk cache), not an // error. func TestCacheMaxBytesZeroIsValidAndDisablesCache(t *testing.T) { yamlContent := "signing_key: " + validTestSigningKey + "\ncache_max_bytes: 0\n" c, err := configFromYAML(t, yamlContent) if err != nil { t.Fatalf("cache_max_bytes: 0 must be accepted, got error: %v", err) } if c.CacheMaxBytes != 0 { t.Errorf("CacheMaxBytes = %d, want 0", c.CacheMaxBytes) } } // TestCacheMaxBytesLargeExplicitValueParses verifies that values above // 32-bit range parse correctly (the field is an int64 byte count). func TestCacheMaxBytesLargeExplicitValueParses(t *testing.T) { yamlContent := "signing_key: " + validTestSigningKey + "\ncache_max_bytes: 10737418240\n" c, err := configFromYAML(t, yamlContent) if err != nil { t.Fatalf("large cache_max_bytes must be accepted, got error: %v", err) } if c.CacheMaxBytes != 10737418240 { t.Errorf("CacheMaxBytes = %d, want 10737418240", c.CacheMaxBytes) } } // TestCacheMaxBytesInvalidValuesAbortStartup verifies that a SET but // invalid cache_max_bytes value aborts startup naming the key and the // offending value, per the no-silent-fallback rule: defaults apply // only to omitted keys. func TestCacheMaxBytesInvalidValuesAbortStartup(t *testing.T) { signingKeyLine := "signing_key: " + validTestSigningKey + "\n" cases := []struct { name string yaml string // wantErrSubstrings must all appear in the error message. wantErrSubstrings []string }{ { name: "negative", yaml: signingKeyLine + "cache_max_bytes: -1024\n", wantErrSubstrings: []string{"cache_max_bytes", "-1024"}, }, { name: "float", yaml: signingKeyLine + "cache_max_bytes: 3.5\n", wantErrSubstrings: []string{"cache_max_bytes", "3.5"}, }, { name: "non-numeric string", yaml: signingKeyLine + "cache_max_bytes: banana\n", wantErrSubstrings: []string{"cache_max_bytes", "banana"}, }, { name: "explicit null", yaml: signingKeyLine + "cache_max_bytes: null\n", wantErrSubstrings: []string{"cache_max_bytes", "null"}, }, { name: "bare key no value", yaml: signingKeyLine + "cache_max_bytes:\n", wantErrSubstrings: []string{"cache_max_bytes", "null"}, }, { name: "boolean", yaml: signingKeyLine + "cache_max_bytes: true\n", wantErrSubstrings: []string{"cache_max_bytes", "true"}, }, { name: "list", yaml: signingKeyLine + "cache_max_bytes:\n - 1\n", wantErrSubstrings: []string{"cache_max_bytes"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { c, err := configFromYAML(t, tc.yaml) if err == nil { t.Fatalf("config with %s cache_max_bytes must abort startup, got config: %+v", tc.name, c) } t.Logf("got expected error: %v", err) for _, want := range tc.wantErrSubstrings { if !strings.Contains(err.Error(), want) { t.Errorf("error %q does not mention %q", err.Error(), want) } } }) } } // TestComputeDefaultCacheMaxBytesUses75PercentOfFreeSpace verifies the // computed default is 75% of the probed free space when that exceeds // the floor. func TestComputeDefaultCacheMaxBytesUses75PercentOfFreeSpace(t *testing.T) { // 4 GiB free -> 3 GiB default. probe := func(string) (uint64, error) { return 4294967296, nil } got, err := ComputeDefaultCacheMaxBytes(t.TempDir(), probe) if err != nil { t.Fatalf("ComputeDefaultCacheMaxBytes returned error: %v", err) } if got != 3221225472 { t.Errorf("ComputeDefaultCacheMaxBytes = %d, want 3221225472 (75%% of 4 GiB)", got) } } // TestComputeDefaultCacheMaxBytesAppliesFloorToComputedDefault // verifies that when 75% of free space is below 500 MiB, the computed // default is floored at DefaultCacheMaxBytesFloor. func TestComputeDefaultCacheMaxBytesAppliesFloorToComputedDefault(t *testing.T) { cases := []struct { name string freeBytes uint64 }{ {name: "100 MiB free", freeBytes: 104857600}, {name: "zero free", freeBytes: 0}, {name: "just below floor threshold", freeBytes: 699050665}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { probe := func(string) (uint64, error) { return tc.freeBytes, nil } got, err := ComputeDefaultCacheMaxBytes(t.TempDir(), probe) if err != nil { t.Fatalf("ComputeDefaultCacheMaxBytes returned error: %v", err) } if got != DefaultCacheMaxBytesFloor { t.Errorf("ComputeDefaultCacheMaxBytes = %d, want floor %d", got, DefaultCacheMaxBytesFloor) } }) } } // TestComputeDefaultCacheMaxBytesPropagatesProbeError verifies that a // failing free-space probe produces an error naming the config key, // instead of a silently wrong default. func TestComputeDefaultCacheMaxBytesPropagatesProbeError(t *testing.T) { probe := func(string) (uint64, error) { return 0, errors.New("statfs failed") } _, err := ComputeDefaultCacheMaxBytes(t.TempDir(), probe) if err == nil { t.Fatal("probe failure must produce an error, got nil") } t.Logf("got expected error: %v", err) if !strings.Contains(err.Error(), "cache_max_bytes") { t.Errorf("error %q does not name the config key cache_max_bytes", err.Error()) } } // TestResolveCacheMaxBytesComputesDefaultWhenOmitted verifies that an // omitted cache_max_bytes key resolves to the computed default, that // the probe is pointed at /cache/ (which must be created // first so statfs measures the right filesystem), and that the result // lands on the Config. func TestResolveCacheMaxBytesComputesDefaultWhenOmitted(t *testing.T) { c, err := configFromYAML(t, "signing_key: "+validTestSigningKey+"\n") if err != nil { t.Fatalf("minimal config should be valid, got error: %v", err) } c.StateDir = t.TempDir() wantCacheDir := filepath.Join(c.StateDir, "cache") var probedPath string // 4 GiB free -> 3 GiB default. probe := func(path string) (uint64, error) { probedPath = path return 4294967296, nil } if err := c.resolveCacheMaxBytes(discardLogger(), probe); err != nil { t.Fatalf("resolveCacheMaxBytes returned error: %v", err) } if c.CacheMaxBytes != 3221225472 { t.Errorf("CacheMaxBytes = %d, want computed default 3221225472", c.CacheMaxBytes) } if probedPath != wantCacheDir { t.Errorf("free space probed at %q, want cache directory %q", probedPath, wantCacheDir) } info, err := os.Stat(wantCacheDir) if err != nil || !info.IsDir() { t.Errorf("cache directory %q was not created before probing: info=%v err=%v", wantCacheDir, info, err) } } // TestResolveCacheMaxBytesDoesNotOverrideExplicitValue verifies that // an explicitly configured value survives resolution untouched and // that the free-space probe is never consulted for it. func TestResolveCacheMaxBytesDoesNotOverrideExplicitValue(t *testing.T) { yamlContent := "signing_key: " + validTestSigningKey + "\ncache_max_bytes: 1024\n" c, err := configFromYAML(t, yamlContent) if err != nil { t.Fatalf("explicit cache_max_bytes must be accepted, got error: %v", err) } c.StateDir = t.TempDir() probe := func(string) (uint64, error) { t.Error("free-space probe must not be consulted for explicit values") return 0, errors.New("probe must not be called") } if err := c.resolveCacheMaxBytes(discardLogger(), probe); err != nil { t.Fatalf("resolveCacheMaxBytes returned error: %v", err) } if c.CacheMaxBytes != 1024 { t.Errorf("CacheMaxBytes = %d, want explicit 1024 (no floor, no recompute)", c.CacheMaxBytes) } }