328 lines
7.5 KiB
Go
328 lines
7.5 KiB
Go
package memory
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMemoryFS_Basic(t *testing.T) {
|
|
t.Parallel()
|
|
m := New(1024 * 1024)
|
|
if m.Name() != "MemoryFS" {
|
|
t.Error("bad name")
|
|
}
|
|
if m.Capacity() != 1024*1024 {
|
|
t.Error("bad cap")
|
|
}
|
|
|
|
w, err := m.Create("k1", 100)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
n, _ := w.Write(make([]byte, 100))
|
|
w.Close()
|
|
if n != 100 {
|
|
t.Error("write len")
|
|
}
|
|
if m.Size() != 100 {
|
|
t.Errorf("size=%d want 100", m.Size())
|
|
}
|
|
|
|
r, err := m.Open("k1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := io.ReadAll(r)
|
|
r.Close()
|
|
if len(data) != 100 {
|
|
t.Error("read mismatch")
|
|
}
|
|
|
|
if err := m.Delete("k1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := m.Open("k1"); err == nil {
|
|
t.Error("deleted key still openable")
|
|
}
|
|
}
|
|
|
|
func TestMemoryFS_EvictUnderPressure(t *testing.T) {
|
|
t.Parallel()
|
|
m := New(500)
|
|
// create 3x200 = 600 >500, should trigger internal? but direct evict call
|
|
for i := 0; i < 3; i++ {
|
|
w, _ := m.Create("f"+string(rune('0'+i)), 200)
|
|
w.Write(make([]byte, 200))
|
|
w.Close()
|
|
}
|
|
// force evict
|
|
evicted := m.EvictLRU(100)
|
|
if evicted == 0 || m.Size() > 500 {
|
|
t.Errorf("evict failed: evicted=%d size=%d", evicted, m.Size())
|
|
}
|
|
}
|
|
|
|
func TestMemoryFS_SizeNeverExceedsAfterEvict(t *testing.T) {
|
|
t.Parallel()
|
|
cap := int64(1000)
|
|
m := New(cap)
|
|
// Strengthened: cycle through strategies (randomized feel via mod), use testKey, stricter post-evict with documented epsilon (issue9)
|
|
strats := []func(uint) uint{m.EvictLRU, func(n uint) uint { return m.EvictBySize(n, true) }, m.EvictFIFO, m.EvictLFU, m.EvictHybrid}
|
|
for i := 0; i < 50; i++ { // more cycles
|
|
sz := int64(100 + i%50)
|
|
w, err := m.Create(testKey(i), sz)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.Write(make([]byte, sz))
|
|
w.Close()
|
|
// Raw MemoryFS allows temporary over (enforced by GCFS wrapper in real use).
|
|
// Force evict under pressure and verify post-evict invariant.
|
|
if m.Size() > cap-50 {
|
|
fn := strats[i%len(strats)]
|
|
fn(200)
|
|
if m.Size() > cap+50 { // RLock snapshot + batch may temporarily exceed; GC layer enforces strict limit
|
|
t.Fatalf("size %d >> cap %d after evict", m.Size(), cap)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMemoryFS_ConcurrentCreateOpenDelete(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip()
|
|
}
|
|
t.Parallel()
|
|
m := New(10 * 1024 * 1024)
|
|
var wg sync.WaitGroup
|
|
const N = 50
|
|
var ops int64
|
|
for i := 0; i < 8; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < N; j++ {
|
|
key := "c" + string(rune('a'+id)) + string(rune(j%10))
|
|
w, err := m.Create(key, 128)
|
|
if err == nil {
|
|
w.Write(make([]byte, 128))
|
|
w.Close()
|
|
atomic.AddInt64(&ops, 1)
|
|
}
|
|
if r, err := m.Open(key); err == nil {
|
|
io.Copy(io.Discard, r)
|
|
r.Close()
|
|
atomic.AddInt64(&ops, 1)
|
|
}
|
|
_ = m.Delete(key)
|
|
atomic.AddInt64(&ops, 1)
|
|
if j%10 == 0 {
|
|
m.EvictLRU(256)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
if ops < 100 {
|
|
t.Errorf("too few concurrent ops: %d", ops)
|
|
}
|
|
// size should be bounded
|
|
if m.Size() > m.Capacity() {
|
|
t.Errorf("final size %d > cap", m.Size())
|
|
}
|
|
}
|
|
|
|
func BenchmarkMemoryFS_CreateOpen(b *testing.B) {
|
|
m := New(64 * 1024 * 1024)
|
|
data := make([]byte, 4096)
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
key := "b" + string(rune(i%1000))
|
|
w, _ := m.Create(key, 4096)
|
|
w.Write(data)
|
|
w.Close()
|
|
r, _ := m.Open(key)
|
|
io.Copy(io.Discard, r)
|
|
r.Close()
|
|
_ = m.Delete(key)
|
|
}
|
|
}
|
|
|
|
func BenchmarkEvictionUnderPressure(b *testing.B) {
|
|
m := New(1 * 1024 * 1024)
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
// fill then evict (setup fill not timed separately to keep bench focused on pressure+evict cycle)
|
|
for j := 0; j < 20; j++ {
|
|
w, _ := m.Create("e"+string(rune(j)), 64*1024)
|
|
w.Write(make([]byte, 64*1024))
|
|
w.Close()
|
|
}
|
|
m.EvictLRU(512 * 1024)
|
|
}
|
|
_ = m // keep
|
|
}
|
|
|
|
func TestMemoryFS_Stats(t *testing.T) {
|
|
t.Parallel()
|
|
m := New(1024)
|
|
stats := m.GetFragmentationStats()
|
|
if stats["buffer_count"] != 0 {
|
|
t.Error("initial buffers >0?")
|
|
}
|
|
}
|
|
|
|
// testKey helper (addresses brittle keygen nit21 across tests).
|
|
func testKey(i int) string {
|
|
return fmt.Sprintf("test/key/%04d", i)
|
|
}
|
|
|
|
// TestMemoryFS_ConcurrentCloseAndEvict_RaceFree is a synthetic load test exercising concurrent Close during eviction (validates the R/W split fixes).
|
|
// Exercises overlapping writer Close() (mutates fi.Size under W) + all Evict* strategies under load.
|
|
// Must be -race clean; also strengthens property coverage.
|
|
func TestMemoryFS_ConcurrentCloseAndEvict_RaceFree(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip()
|
|
}
|
|
t.Parallel()
|
|
m := New(2 * 1024 * 1024) // 2MB
|
|
var wg sync.WaitGroup
|
|
stopCh := make(chan struct{})
|
|
const writers = 3
|
|
const evictors = 3
|
|
|
|
// Writers: create + write + close (triggers size mutation in Close)
|
|
for i := 0; i < writers; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; ; j++ {
|
|
select {
|
|
case <-stopCh:
|
|
return
|
|
default:
|
|
}
|
|
key := testKey(id*10000 + j)
|
|
w, err := m.Create(key, 4096)
|
|
if err == nil {
|
|
w.Write(make([]byte, 4096))
|
|
w.Close() // mutates live *FileInfo.Size + global size (race target)
|
|
}
|
|
if j%5 == 0 {
|
|
m.Delete(key)
|
|
}
|
|
if j > 100 {
|
|
break // bound per writer
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Evictors: hammer all 5 strategies + LRU (exercises snapshot copy + live re-fetch + short LRU Lock)
|
|
strats := []func(uint) uint{
|
|
m.EvictLRU,
|
|
func(n uint) uint { return m.EvictBySize(n, true) },
|
|
func(n uint) uint { return m.EvictBySize(n, false) },
|
|
m.EvictFIFO,
|
|
m.EvictLFU,
|
|
m.EvictHybrid,
|
|
}
|
|
for i := 0; i < evictors; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; ; j++ {
|
|
select {
|
|
case <-stopCh:
|
|
return
|
|
default:
|
|
}
|
|
s := strats[j%len(strats)]
|
|
s(1024)
|
|
if j > 50 {
|
|
break
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
time.Sleep(150 * time.Millisecond) // load duration; bounded
|
|
close(stopCh)
|
|
wg.Wait()
|
|
|
|
// Post-run invariants (loose due to raw MemoryFS overcommit design; GCFS enforces)
|
|
if m.Size() < 0 {
|
|
t.Error("negative size after concurrent close+evict")
|
|
}
|
|
// LRU len reasonable
|
|
_ = m.LRU.Len()
|
|
}
|
|
|
|
func TestMemoryFS_EvictVariantsAndErrors(t *testing.T) {
|
|
t.Parallel()
|
|
m := New(800)
|
|
// populate
|
|
for i := 0; i < 4; i++ {
|
|
w, _ := m.Create("ev"+string(rune('0'+i)), 150)
|
|
w.Write(make([]byte, 150))
|
|
w.Close()
|
|
}
|
|
_ = m.EvictBySize(100, true) // smallest
|
|
_ = m.EvictFIFO(50)
|
|
_ = m.EvictLFU(50)
|
|
_ = m.EvictHybrid(50)
|
|
|
|
// invalid keys
|
|
if _, err := m.Create("", 1); err == nil {
|
|
t.Error("empty key allowed")
|
|
}
|
|
if _, err := m.Create("/abs", 1); err == nil {
|
|
t.Error("abs key allowed")
|
|
}
|
|
if _, err := m.Create("..bad", 1); err == nil {
|
|
t.Error("traversal key allowed")
|
|
}
|
|
if _, err := m.Open("nope"); err == nil {
|
|
t.Error("open missing")
|
|
}
|
|
if err := m.Delete("nope"); err == nil {
|
|
t.Error("delete missing")
|
|
}
|
|
if _, err := m.Stat("nope"); err == nil {
|
|
t.Error("stat missing")
|
|
}
|
|
// overwrite path + actual size update via closer
|
|
w2, _ := m.Create("ow", 10)
|
|
w2.Write([]byte{1, 2, 3})
|
|
w2.Close() // updates to real 3
|
|
if fi, _ := m.Stat("ow"); fi.Size != 3 {
|
|
t.Errorf("overwrite size %d !=3", fi.Size)
|
|
}
|
|
// hit fragmentation stats after activity
|
|
_ = m.GetFragmentationStats()
|
|
}
|
|
|
|
func TestMemoryFS_AllEvictStrategies(t *testing.T) {
|
|
t.Parallel()
|
|
m := New(300)
|
|
for i := 0; i < 3; i++ {
|
|
w, _ := m.Create("s"+string(rune(i)), 120)
|
|
w.Write(make([]byte, 120))
|
|
w.Close()
|
|
}
|
|
_ = m.EvictBySize(50, true)
|
|
_ = m.EvictBySize(50, false)
|
|
_ = m.EvictFIFO(20)
|
|
_ = m.EvictLFU(20)
|
|
_ = m.EvictHybrid(20)
|
|
if m.Size() > m.Capacity() {
|
|
t.Error("post variant evict over cap")
|
|
}
|
|
}
|