diff --git a/plugins/cache_purge_test.go b/plugins/cache_purge_test.go deleted file mode 100644 index 1afe26d41..000000000 --- a/plugins/cache_purge_test.go +++ /dev/null @@ -1,189 +0,0 @@ -//go:build !windows - -package plugins - -import ( - "context" - "os" - "path/filepath" - "time" - - "github.com/dustin/go-humanize" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("purgeCacheBySize", func() { - var ( - tmpDir string - ctx context.Context - ) - - BeforeEach(func() { - var err error - ctx = GinkgoT().Context() - tmpDir, err = os.MkdirTemp("", "cache-purge-test-*") - Expect(err).ToNot(HaveOccurred()) - }) - - AfterEach(func() { - os.RemoveAll(tmpDir) - }) - - createFileWithSize := func(path string, sizeBytes int64, modTime time.Time) { - dir := filepath.Dir(path) - err := os.MkdirAll(dir, 0755) - Expect(err).ToNot(HaveOccurred()) - - f, err := os.Create(path) - Expect(err).ToNot(HaveOccurred()) - defer f.Close() - - // Write random data to reach desired size - if sizeBytes > 0 { - err = f.Truncate(sizeBytes) - Expect(err).ToNot(HaveOccurred()) - } - - // Set modification time - err = os.Chtimes(path, modTime, modTime) - Expect(err).ToNot(HaveOccurred()) - } - - getDirSize := func(dir string) uint64 { - var total uint64 - err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { - if err != nil || d.IsDir() { - return nil - } - info, err := d.Info() - if err != nil { - return nil - } - total += uint64(info.Size()) - return nil - }) - Expect(err).ToNot(HaveOccurred()) - return total - } - - Context("when maxSize is invalid or zero", func() { - It("should not remove any files with invalid size", func() { - cacheDir := filepath.Join(tmpDir, "cache") - createFileWithSize(filepath.Join(cacheDir, "file1.bin"), 1000, time.Now()) - createFileWithSize(filepath.Join(cacheDir, "file2.bin"), 1000, time.Now()) - - purgeCacheBySize(ctx, cacheDir, "invalid") - - Expect(getDirSize(cacheDir)).To(Equal(uint64(2000))) - }) - - It("should not remove any files when maxSize is 0", func() { - cacheDir := filepath.Join(tmpDir, "cache") - createFileWithSize(filepath.Join(cacheDir, "file1.bin"), 1000, time.Now()) - createFileWithSize(filepath.Join(cacheDir, "file2.bin"), 1000, time.Now()) - - purgeCacheBySize(ctx, cacheDir, "0") - - Expect(getDirSize(cacheDir)).To(Equal(uint64(2000))) - }) - }) - - Context("when cache directory doesn't exist", func() { - It("should not error", func() { - nonExistentDir := filepath.Join(tmpDir, "nonexistent") - Expect(func() { - purgeCacheBySize(ctx, nonExistentDir, "100MB") - }).ToNot(Panic()) - }) - }) - - Context("when total size is under limit", func() { - It("should not remove any files", func() { - cacheDir := filepath.Join(tmpDir, "cache") - createFileWithSize(filepath.Join(cacheDir, "file1.bin"), 1000, time.Now()) - createFileWithSize(filepath.Join(cacheDir, "file2.bin"), 1000, time.Now()) - - purgeCacheBySize(ctx, cacheDir, "10KB") - - Expect(getDirSize(cacheDir)).To(Equal(uint64(2000))) - }) - }) - - Context("when total size exceeds limit", func() { - It("should remove oldest files first", func() { - cacheDir := filepath.Join(tmpDir, "cache") - now := time.Now() - - // Create files with different ages (1MB each) - oldestFile := filepath.Join(cacheDir, "old.bin") - middleFile := filepath.Join(cacheDir, "middle.bin") - newestFile := filepath.Join(cacheDir, "new.bin") - - createFileWithSize(oldestFile, 1*1024*1024, now.Add(-3*time.Hour)) - createFileWithSize(middleFile, 1*1024*1024, now.Add(-2*time.Hour)) - createFileWithSize(newestFile, 1*1024*1024, now.Add(-1*time.Hour)) - - // Set limit to 2MiB - should remove oldest file - purgeCacheBySize(ctx, cacheDir, "2MiB") - - // Oldest should be removed - _, err := os.Stat(oldestFile) - Expect(os.IsNotExist(err)).To(BeTrue(), "oldest file should be removed") - - // Others should remain - _, err = os.Stat(middleFile) - Expect(err).ToNot(HaveOccurred(), "middle file should remain") - - _, err = os.Stat(newestFile) - Expect(err).ToNot(HaveOccurred(), "newest file should remain") - }) - - It("should remove multiple files to get under limit", func() { - cacheDir := filepath.Join(tmpDir, "cache") - now := time.Now() - - // Create 5 files, 1MiB each (total 5MiB) - for i := 0; i < 5; i++ { - path := filepath.Join(cacheDir, filepath.Join("dir", "file"+string(rune('0'+i))+".bin")) - createFileWithSize(path, 1*1024*1024, now.Add(-time.Duration(5-i)*time.Hour)) - } - - // Set limit to 2.5MiB - should remove oldest 3 files (leaving 2MiB) - purgeCacheBySize(ctx, cacheDir, "2.5MiB") - - finalSize := getDirSize(cacheDir) - limit, _ := humanize.ParseBytes("2.5MiB") - Expect(finalSize).To(BeNumerically("<=", limit)) - }) - - It("should remove empty parent directories after removing files", func() { - cacheDir := filepath.Join(tmpDir, "cache") - now := time.Now() - - // Create files in subdirectories - oldFile := filepath.Join(cacheDir, "subdir1", "old.bin") - newFile := filepath.Join(cacheDir, "subdir2", "new.bin") - - createFileWithSize(oldFile, 2*1024*1024, now.Add(-2*time.Hour)) - createFileWithSize(newFile, 2*1024*1024, now.Add(-1*time.Hour)) - - // Set limit to 2MiB - should remove old file and its parent dir - purgeCacheBySize(ctx, cacheDir, "2MiB") - - // Old file and its parent dir should be removed - _, err := os.Stat(oldFile) - Expect(os.IsNotExist(err)).To(BeTrue()) - - _, err = os.Stat(filepath.Join(cacheDir, "subdir1")) - Expect(os.IsNotExist(err)).To(BeTrue(), "empty parent directory should be removed") - - // New file and its parent dir should remain - _, err = os.Stat(newFile) - Expect(err).ToNot(HaveOccurred()) - - _, err = os.Stat(filepath.Join(cacheDir, "subdir2")) - Expect(err).ToNot(HaveOccurred()) - }) - }) -}) diff --git a/plugins/manager_test.go b/plugins/manager_test.go index 78b9e214e..13d9b46c5 100644 --- a/plugins/manager_test.go +++ b/plugins/manager_test.go @@ -3,8 +3,12 @@ package plugins import ( "context" "fmt" + "os" + "path/filepath" "sync" + "time" + "github.com/dustin/go-humanize" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/core/agents" . "github.com/onsi/ginkgo/v2" @@ -153,3 +157,178 @@ var _ = Describe("Manager", Ordered, func() { } }) }) + +var _ = Describe("purgeCacheBySize", func() { + var ( + tmpDir string + ctx context.Context + ) + + BeforeEach(func() { + var err error + ctx = GinkgoT().Context() + tmpDir, err = os.MkdirTemp("", "cache-purge-test-*") + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + os.RemoveAll(tmpDir) + }) + + createFileWithSize := func(path string, sizeBytes int64, modTime time.Time) { + dir := filepath.Dir(path) + err := os.MkdirAll(dir, 0755) + Expect(err).ToNot(HaveOccurred()) + + f, err := os.Create(path) + Expect(err).ToNot(HaveOccurred()) + defer f.Close() + + // Write random data to reach desired size + if sizeBytes > 0 { + err = f.Truncate(sizeBytes) + Expect(err).ToNot(HaveOccurred()) + } + + // Set modification time + err = os.Chtimes(path, modTime, modTime) + Expect(err).ToNot(HaveOccurred()) + } + + getDirSize := func(dir string) uint64 { + var total uint64 + err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() { + return nil + } + info, err := d.Info() + if err != nil { + return nil + } + total += uint64(info.Size()) + return nil + }) + Expect(err).ToNot(HaveOccurred()) + return total + } + + Context("when maxSize is invalid or zero", func() { + It("should not remove any files with invalid size", func() { + cacheDir := filepath.Join(tmpDir, "cache") + createFileWithSize(filepath.Join(cacheDir, "file1.bin"), 1000, time.Now()) + createFileWithSize(filepath.Join(cacheDir, "file2.bin"), 1000, time.Now()) + + purgeCacheBySize(ctx, cacheDir, "invalid") + + Expect(getDirSize(cacheDir)).To(Equal(uint64(2000))) + }) + + It("should not remove any files when maxSize is 0", func() { + cacheDir := filepath.Join(tmpDir, "cache") + createFileWithSize(filepath.Join(cacheDir, "file1.bin"), 1000, time.Now()) + createFileWithSize(filepath.Join(cacheDir, "file2.bin"), 1000, time.Now()) + + purgeCacheBySize(ctx, cacheDir, "0") + + Expect(getDirSize(cacheDir)).To(Equal(uint64(2000))) + }) + }) + + Context("when cache directory doesn't exist", func() { + It("should not error", func() { + nonExistentDir := filepath.Join(tmpDir, "nonexistent") + Expect(func() { + purgeCacheBySize(ctx, nonExistentDir, "100MB") + }).ToNot(Panic()) + }) + }) + + Context("when total size is under limit", func() { + It("should not remove any files", func() { + cacheDir := filepath.Join(tmpDir, "cache") + createFileWithSize(filepath.Join(cacheDir, "file1.bin"), 1000, time.Now()) + createFileWithSize(filepath.Join(cacheDir, "file2.bin"), 1000, time.Now()) + + purgeCacheBySize(ctx, cacheDir, "10KB") + + Expect(getDirSize(cacheDir)).To(Equal(uint64(2000))) + }) + }) + + Context("when total size exceeds limit", func() { + It("should remove oldest files first", func() { + cacheDir := filepath.Join(tmpDir, "cache") + now := time.Now() + + // Create files with different ages (1MB each) + oldestFile := filepath.Join(cacheDir, "old.bin") + middleFile := filepath.Join(cacheDir, "middle.bin") + newestFile := filepath.Join(cacheDir, "new.bin") + + createFileWithSize(oldestFile, 1*1024*1024, now.Add(-3*time.Hour)) + createFileWithSize(middleFile, 1*1024*1024, now.Add(-2*time.Hour)) + createFileWithSize(newestFile, 1*1024*1024, now.Add(-1*time.Hour)) + + // Set limit to 2MiB - should remove oldest file + purgeCacheBySize(ctx, cacheDir, "2MiB") + + // Oldest should be removed + _, err := os.Stat(oldestFile) + Expect(os.IsNotExist(err)).To(BeTrue(), "oldest file should be removed") + + // Others should remain + _, err = os.Stat(middleFile) + Expect(err).ToNot(HaveOccurred(), "middle file should remain") + + _, err = os.Stat(newestFile) + Expect(err).ToNot(HaveOccurred(), "newest file should remain") + }) + + It("should remove multiple files to get under limit", func() { + cacheDir := filepath.Join(tmpDir, "cache") + now := time.Now() + + // Create 5 files, 1MiB each (total 5MiB) + for i := 0; i < 5; i++ { + path := filepath.Join(cacheDir, filepath.Join("dir", "file"+string(rune('0'+i))+".bin")) + createFileWithSize(path, 1*1024*1024, now.Add(-time.Duration(5-i)*time.Hour)) + } + + // Set limit to 2.5MiB - should remove oldest 3 files (leaving 2MiB) + purgeCacheBySize(ctx, cacheDir, "2.5MiB") + + finalSize := getDirSize(cacheDir) + limit, _ := humanize.ParseBytes("2.5MiB") + Expect(finalSize).To(BeNumerically("<=", limit)) + }) + + It("should remove empty parent directories after removing files", func() { + cacheDir := filepath.Join(tmpDir, "cache") + now := time.Now() + + // Create files in subdirectories + oldFile := filepath.Join(cacheDir, "subdir1", "old.bin") + newFile := filepath.Join(cacheDir, "subdir2", "new.bin") + + createFileWithSize(oldFile, 2*1024*1024, now.Add(-2*time.Hour)) + createFileWithSize(newFile, 2*1024*1024, now.Add(-1*time.Hour)) + + // Set limit to 2MiB - should remove old file and its parent dir + purgeCacheBySize(ctx, cacheDir, "2MiB") + + // Old file and its parent dir should be removed + _, err := os.Stat(oldFile) + Expect(os.IsNotExist(err)).To(BeTrue()) + + _, err = os.Stat(filepath.Join(cacheDir, "subdir1")) + Expect(os.IsNotExist(err)).To(BeTrue(), "empty parent directory should be removed") + + // New file and its parent dir should remain + _, err = os.Stat(newFile) + Expect(err).ToNot(HaveOccurred()) + + _, err = os.Stat(filepath.Join(cacheDir, "subdir2")) + Expect(err).ToNot(HaveOccurred()) + }) + }) +})