package zitcli
import (
"strings"
"testing"
)
func diffOf(n int) diffResult {
return diffResult{
unified: strings.Repeat("x", n),
entries: []DiffEntry{{Status: "M", Name: "a.go"}},
}
}
// The bound that matters is bytes. A cap counted in entries lets a
// handful of large merges hold far more memory than the budget any
// operator chose — the same failure the blob cache's note describes,
// and what the old 32-entry diff memo actually did.
func TestTheDiffMemoIsBoundedByBytesNotEntries(t *testing.T) {
c := newDiffCache(1000)
// Three entries, but together far past the budget.
c.put("a", diffOf(400))
c.put("b", diffOf(400))
c.put("c", diffOf(400))
entries, bytes := c.stats()
if bytes > 1000 {
t.Errorf("FAIL: holding %d bytes over a 1000-byte budget", bytes)
}
if entries == 3 {
t.Error("FAIL: three oversized diffs all survived — the budget is not being enforced")
}
// The oldest went, not a random one: eviction is least-recently-used.
if _, ok := c.get("a"); ok {
t.Error("FAIL: the least recently used entry survived")
}
if _, ok := c.get("c"); !ok {
t.Error("FAIL: the most recently used entry was evicted")
}
}
// A diff that has been read recently must outlive one that has not,
// whatever order they arrived in. The old memo deleted whichever key the
// map iterator happened to yield, so a hot diff was evicted as readily
// as a cold one.
func TestAReadDiffOutlivesAnUnreadOne(t *testing.T) {
c := newDiffCache(1000)
c.put("old", diffOf(300))
c.put("mid", diffOf(300))
// Reading "old" makes it the most recent, so the next eviction must
// take "mid" instead.
if _, ok := c.get("old"); !ok {
t.Fatal("FAIL: setup — old was not stored")
}
c.put("new", diffOf(300))
c.put("newer", diffOf(300))
if _, ok := c.get("mid"); ok {
t.Error("FAIL: the unread entry survived while a read one was evicted")
}
if _, ok := c.get("old"); !ok {
t.Error("FAIL: reading an entry did not protect it from eviction")
}
}
// A single diff larger than the whole budget is not cached, and is not
// an error. Admitting it would evict everything to hold one item, which
// is worse than missing.
func TestAnOversizedDiffIsNotCached(t *testing.T) {
c := newDiffCache(100)
c.put("huge", diffOf(5000))
if entries, bytes := c.stats(); entries != 0 || bytes != 0 {
t.Errorf("FAIL: an oversized diff was admitted (%d entries, %d bytes)", entries, bytes)
}
if _, ok := c.get("huge"); ok {
t.Error("FAIL: an oversized diff reads back as a hit")
}
}
// The entry list counts toward the size. A merge across a thousand
// files carries a thousand of them, and ignoring that would let the
// memo hold well past its budget on wide, shallow diffs.
func TestTheFileListCountsTowardTheBudget(t *testing.T) {
wide := diffResult{unified: ""}
for i := 0; i < 500; i++ {
wide.entries = append(wide.entries, DiffEntry{Status: "M", Name: "some/long/path/file.go"})
}
c := newDiffCache(1 << 20)
c.put("wide", wide)
if _, bytes := c.stats(); bytes == 0 {
t.Error("FAIL: a diff with 500 files and no hunks was counted as weighing nothing")
}
}
// A nil memo is a no-op, not a panic: a Client built without one still
// diffs, it just pays every time.
func TestANilDiffMemoIsHarmless(t *testing.T) {
var c *diffCache
c.put("a", diffOf(10))
if _, ok := c.get("a"); ok {
t.Error("FAIL: a nil memo returned a hit")
}
if entries, bytes := c.stats(); entries != 0 || bytes != 0 {
t.Errorf("FAIL: a nil memo reported %d entries, %d bytes", entries, bytes)
}
}