package daemon import ( "context" "os" "path/filepath" "strings" "testing" "banger/internal/api" "banger/internal/paths" ) // seedFakeOCICache drops a few fixed-size files that mimic an OCI // layer cache layout (blobs/sha256/) so tests don't depend on // real registry round-trips. func seedFakeOCICache(t *testing.T, cacheDir string) (totalBytes int64, blobCount int) { t.Helper() blobsDir := filepath.Join(cacheDir, "blobs", "sha256") if err := os.MkdirAll(blobsDir, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } for i, payload := range []string{"layer-a", "layer-b-bigger", "layer-c"} { name := strings.Repeat("ab", 32) // 64 hex chars stand-in path := filepath.Join(blobsDir, name+"-"+string(rune('0'+i))) if err := os.WriteFile(path, []byte(payload), 0o644); err != nil { t.Fatalf("write blob: %v", err) } totalBytes += int64(len(payload)) blobCount++ } return totalBytes, blobCount } func TestPruneOCICacheDryRunReportsSizeWithoutDeleting(t *testing.T) { cacheRoot := t.TempDir() cacheDir := filepath.Join(cacheRoot, "oci") wantBytes, wantBlobs := seedFakeOCICache(t, cacheDir) d := &Daemon{layout: paths.Layout{OCICacheDir: cacheDir}} wireServices(d) res, err := d.img.PruneOCICache(context.Background(), api.ImageCachePruneParams{DryRun: true}) if err != nil { t.Fatalf("PruneOCICache: %v", err) } if res.BytesFreed != wantBytes { t.Fatalf("BytesFreed = %d, want %d", res.BytesFreed, wantBytes) } if res.BlobsFreed != wantBlobs { t.Fatalf("BlobsFreed = %d, want %d", res.BlobsFreed, wantBlobs) } if !res.DryRun { t.Error("result.DryRun = false, want true") } // Blobs must still exist. entries, _ := os.ReadDir(filepath.Join(cacheDir, "blobs", "sha256")) if len(entries) != wantBlobs { t.Fatalf("blobs dir: got %d entries, want %d (dry-run must not delete)", len(entries), wantBlobs) } } func TestPruneOCICacheRemovesAllBlobs(t *testing.T) { cacheRoot := t.TempDir() cacheDir := filepath.Join(cacheRoot, "oci") wantBytes, wantBlobs := seedFakeOCICache(t, cacheDir) d := &Daemon{layout: paths.Layout{OCICacheDir: cacheDir}} wireServices(d) res, err := d.img.PruneOCICache(context.Background(), api.ImageCachePruneParams{}) if err != nil { t.Fatalf("PruneOCICache: %v", err) } if res.BytesFreed != wantBytes { t.Fatalf("BytesFreed = %d, want %d", res.BytesFreed, wantBytes) } if res.BlobsFreed != wantBlobs { t.Fatalf("BlobsFreed = %d, want %d", res.BlobsFreed, wantBlobs) } if res.DryRun { t.Error("result.DryRun = true on a real prune") } // Cache dir must exist (recreated empty) so the next pull has a // place to write blobs. info, err := os.Stat(cacheDir) if err != nil { t.Fatalf("cache dir gone after prune: %v", err) } if !info.IsDir() { t.Fatal("cache path is not a directory after prune") } // Blobs subdir is gone (the rename took everything aside; the // recreate left only the bare cache dir). if _, err := os.Stat(filepath.Join(cacheDir, "blobs")); !os.IsNotExist(err) { t.Fatalf("blobs dir survived prune: %v", err) } // Aside dirs must have been cleaned up too. roots, _ := os.ReadDir(cacheRoot) for _, e := range roots { if strings.Contains(e.Name(), ".pruning-") { t.Errorf("aside dir leaked: %s", e.Name()) } } } // TestPruneOCICacheMissingDirIsZeroResult covers the fresh-install // case: no OCI pulls have ever happened, so the cache dir doesn't // exist. Prune must report zero, not error. func TestPruneOCICacheMissingDirIsZeroResult(t *testing.T) { cacheRoot := t.TempDir() cacheDir := filepath.Join(cacheRoot, "oci") // Don't create cacheDir. d := &Daemon{layout: paths.Layout{OCICacheDir: cacheDir}} wireServices(d) res, err := d.img.PruneOCICache(context.Background(), api.ImageCachePruneParams{}) if err != nil { t.Fatalf("PruneOCICache(missing): %v", err) } if res.BytesFreed != 0 || res.BlobsFreed != 0 { t.Fatalf("missing cache should be zero; got %+v", res) } }