package zitcli
import (
"strings"
"testing"
)
// The budget is in BYTES because the values are file contents: an
// entry-counted cache with a 30MB blob in it is a memory leak with a
// polite name.
func TestBlobCacheEvictsByBytes(t *testing.T) {
c := newBlobCache(100)
c.put("a", make([]byte, 60))
c.put("b", make([]byte, 30))
if n, b := c.stats(); n != 2 || b != 90 {
t.Fatalf("entries=%d bytes=%d, want 2/90", n, b)
}
// Admitting c must evict the least recently used, not the oldest
// inserted — so touch a first.
if _, ok := c.get("a"); !ok {
t.Fatal("a missing")
}
c.put("d", make([]byte, 30))
if _, ok := c.get("b"); ok {
t.Error("evicted the recently-used entry instead of the least-recently-used")
}
if _, ok := c.get("a"); !ok {
t.Error("a was evicted despite being used most recently")
}
if _, b := c.stats(); b > 100 {
t.Errorf("cache holds %d bytes, over its 100-byte budget", b)
}
}
// A value bigger than the whole budget must be refused rather than
// admitted by evicting everything: holding one item at the cost of all
// the others is worse than a miss.
func TestBlobCacheRefusesOversizedValues(t *testing.T) {
c := newBlobCache(100)
c.put("small", []byte("keep me"))
c.put("huge", make([]byte, 500))
if _, ok := c.get("huge"); ok {
t.Error("cached a value larger than the entire budget")
}
if _, ok := c.get("small"); !ok {
t.Error("an oversized put evicted a value that fit")
}
}
// Replacing a key must not double-count its bytes.
func TestBlobCacheReplaceAccountsCorrectly(t *testing.T) {
c := newBlobCache(100)
c.put("k", make([]byte, 40))
c.put("k", make([]byte, 10))
if n, b := c.stats(); n != 1 || b != 10 {
t.Fatalf("entries=%d bytes=%d, want 1/10", n, b)
}
}
// Past the forge-wide ceiling, the cache doing the put gives up its own
// oldest entries; a quiet repository's cache is untouched. This is what
// stopped thirty-three per-repository budgets adding up to more memory
// than the machine had.
func TestCeilingEvictsThePuttingCacheOnly(t *testing.T) {
var f ceiling
f.max.Store(100)
quiet, busy := newBlobCache(1000), newBlobCache(1000)
quiet.ceil, busy.ceil = &f, &f
quiet.put("q", make([]byte, 40))
for _, k := range []string{"a", "b", "c", "d"} {
busy.put(k, make([]byte, 20))
}
if f.used.Load() > 100 {
t.Fatalf("ceiling holds %d bytes, over its 100", f.used.Load())
}
if _, ok := quiet.get("q"); !ok {
t.Fatal("a quiet cache lost its entry to another cache's traffic")
}
if _, ok := busy.get("a"); ok {
t.Error("the busy cache kept its oldest entry past the ceiling")
}
if _, ok := busy.get("d"); !ok {
t.Error("the busy cache dropped its newest entry instead of its oldest")
}
}
// When the putting cache has nothing of its own left to give, the new
// value is not kept, rather than taking the forge over its ceiling.
func TestCeilingRefusesWhenNothingOfOwnToEvict(t *testing.T) {
var f ceiling
f.max.Store(100)
quiet, busy := newBlobCache(1000), newBlobCache(1000)
quiet.ceil, busy.ceil = &f, &f
quiet.put("q", make([]byte, 90))
busy.put("x", make([]byte, 30))
if _, ok := busy.get("x"); ok {
t.Error("cached a value that took the forge past its ceiling")
}
if n, b := busy.stats(); n != 0 || b != 0 || f.used.Load() != 90 {
t.Errorf("busy holds %d/%d, ceiling %d; want 0/0 and 90", n, b, f.used.Load())
}
}
// A blob is charged for the buffer it lives in, not its length: zit's
// output is read into a buffer that grows by doubling.
func TestBlobChargedByCapacity(t *testing.T) {
c := newBlobCache(1 << 20)
c.ceil = &ceiling{}
c.put("k", make([]byte, 10, 64))
if _, b := c.stats(); b != 64 {
t.Errorf("charged %d bytes for a 64-byte buffer", b)
}
}
// Listings are bounded by bytes: one listing of a large store weighs
// more than many of a small one.
func TestListingCacheEvictsByBytes(t *testing.T) {
big := make([]LsEntry, 1000)
for i := range big {
big[i] = LsEntry{Blob: strings.Repeat("b", 64), Name: "some/path/name.c", Perm: "-"}
}
one := newListingCache(0).sizeOf(big)
c := newListingCache(one + one/2)
c.ceil = &ceiling{}
c.put("v1", big)
c.put("v2", big)
if n, b := c.stats(); n != 1 || b > one+one/2 {
t.Fatalf("entries=%d bytes=%d, want 1 within %d", n, b, one+one/2)
}
if _, ok := c.get("v2"); !ok {
t.Error("kept the older listing instead of the newer")
}
}