package disk import ( "io" "sync" "sync/atomic" "testing" "time" ) func TestDiskFS_Basic(t *testing.T) { t.Parallel() td := t.TempDir() d := New(td, 10*1024*1024) if d.Name() != "DiskFS" { t.Error("name") } w, err := d.Create("k1", 50) if err != nil { t.Fatal(err) } w.Write([]byte("hello disk cache test data here")) w.Close() if d.Size() < 30 { // actual may differ slightly from declared t.Errorf("size too small %d", d.Size()) } r, err := d.Open("k1") if err != nil { t.Fatal(err) } data, _ := io.ReadAll(r) r.Close() if len(data) < 10 { t.Error("read small") } d.Delete("k1") if _, err := d.Open("k1"); err == nil { t.Error("deleted still readable") } } func TestDiskFS_EvictAndLazyStat(t *testing.T) { t.Parallel() td := t.TempDir() d := New(td, 400) // create files that will be evicted for i := 0; i < 5; i++ { w, _ := d.Create("f"+string(rune('0'+i)), 120) w.Write(make([]byte, 120)) w.Close() } ev := d.EvictLRU(200) if ev == 0 { t.Log("no evict (size calc async or snapshot tolerance?)") } // lazy stat should still work for remaining; batch eviction may be approximate under heavy pressure if d.Size() > d.Capacity()*2 { // generous for async bg size t.Errorf("disk size %d >> cap after evict", d.Size()) } } func TestDiskFS_Concurrent(t *testing.T) { if testing.Short() { t.Skip() } t.Parallel() td := t.TempDir() d := New(td, 50*1024*1024) var wg sync.WaitGroup var ops int64 for i := 0; i < 4; i++ { wg.Add(1) go func(id int) { defer wg.Done() for j := 0; j < 30; j++ { key := "d" + string(rune(id+'a')) + string(rune(j)) w, e := d.Create(key, 256) if e == nil { w.Write(make([]byte, 256)) w.Close() atomic.AddInt64(&ops, 1) } if r, e := d.Open(key); e == nil { io.Copy(io.Discard, r) r.Close() atomic.AddInt64(&ops, 1) } d.Delete(key) if j%7 == 0 { d.EvictLRU(1024) } } }(i) } wg.Wait() // Bounded poll instead of fixed sleep for bg size calc goroutine settlement (robust to variance; issue7) deadline := time.Now().Add(300 * time.Millisecond) for time.Now().Before(deadline) { if d.Size() <= d.Capacity() { break } time.Sleep(5 * time.Millisecond) } if d.Size() > d.Capacity() { t.Errorf("concurrent disk size exceeded: %d", d.Size()) } } func BenchmarkDiskFS_CreateOpen(b *testing.B) { td := b.TempDir() d := New(td, 128*1024*1024) data := make([]byte, 8192) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { key := "bd" + string(rune(i%500)) w, _ := d.Create(key, 8192) w.Write(data) w.Close() r, _ := d.Open(key) io.Copy(io.Discard, r) r.Close() d.Delete(key) } } func TestDiskFS_EvictVariantsAndInvalid(t *testing.T) { t.Parallel() td := t.TempDir() d := New(td, 600) for i := 0; i < 4; i++ { w, _ := d.Create("dv"+string(rune('0'+i)), 120) w.Write(make([]byte, 120)) w.Close() } _ = d.EvictBySize(80, false) // largest _ = d.EvictFIFO(50) _ = d.EvictLFU(30) _ = d.EvictHybrid(30) // invalids (sanitized in Create/Open) if _, err := d.Create("", 1); err == nil { t.Error("empty") } if _, err := d.Create("/abs/bad", 1); err == nil { t.Error("abs") } if _, err := d.Open("missing"); err == nil { t.Error("missing open") } _ = d.Delete("missing") _, _ = d.Stat("missing") } // TestEvict_ConcurrentCloseDuringEviction exercises Creates, Opens, and Closes (which mutate *FileInfo and size under lock) // concurrently with all Evict* (LRU + non-LRU scalar snapshot paths) on DiskFS under pressure. // Sufficient goroutines/iterations to hit prior race windows for Issues 1-3. Asserts size invariant with // Documented epsilon tolerance for raw DiskFS (background size calc + snapshot tolerance during batch eviction). -race must pass. func TestEvict_ConcurrentCloseDuringEviction(t *testing.T) { if testing.Short() { t.Skip() } t.Parallel() td := t.TempDir() cap := int64(256 * 1024) d := New(td, cap) var wg sync.WaitGroup const nWriters = 4 const nEvictors = 3 const iters = 25 for i := 0; i < nWriters; i++ { wg.Add(1) go func(id int) { defer wg.Done() for j := 0; j < iters; j++ { key := "r" + string(rune('0'+id%5)) + "/" + string(rune('0'+j%10)) w, err := d.Create(key, 8192) if err == nil { w.Write(make([]byte, 4096)) w.Close() // exercises Close size mutation path concurrent with evicts } if r, err := d.Open(key); err == nil { io.Copy(io.Discard, r) r.Close() } if j%4 == 0 { d.Delete(key) } } }(i) } for i := 0; i < nEvictors; i++ { wg.Add(1) go func(id int) { defer wg.Done() for j := 0; j < iters*2; j++ { // Cycle through strategies to cover all snapshot + re-fetch + LRU-Lock paths switch j % 6 { case 0: d.EvictLRU(4096) case 1: d.EvictBySize(4096, true) case 2: d.EvictBySize(4096, false) case 3: d.EvictFIFO(4096) case 4: d.EvictLFU(4096) default: d.EvictHybrid(4096) } } }(i) } wg.Wait() // Final size <= cap with epsilon (raw DiskFS allows small over per bg size + snapshot design; see TestDiskFS_Concurrent and memory +50 pattern) if sz := d.Size(); sz > cap+2048 { t.Errorf("final size %d exceeded cap %d + epsilon tolerance after concurrent close+evict", sz, cap) } }