/
codespawn
/
tgstream
Обзор
Документация
Войти
/
codespawn
/
tgstream
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
16
CI/CD
Аналитика
Безопасность
master
internal/adapter/memorycache/memorycache_test.go
110 строк
4 KB
codespawn
feat: first working version
27 июн 2026, 17:00
27 июн 2026, 17:00
492bb24
Код
Авторство
О чём код?
package memorycache_test import ( "strconv" "sync" "testing" "time" "gitverse.ru/codespawn/tgstream/internal/adapter/memorycache" "gitverse.ru/codespawn/tgstream/internal/domain" "gitverse.ru/codespawn/tgstream/internal/port" "gitverse.ru/codespawn/tgstream/internal/port/porttest" ) // newWithClock — компактный конструктор тестового кэша на контролируемых часах. func newWithClock(t *testing.T, max int) (*memorycache.Cache[[]domain.Post], *porttest.Clock) { t.Helper() clk := porttest.NewClock() c := memorycache.New[[]domain.Post](memorycache.Config{Max: max, Now: clk.Now}) return c, clk } // Контракт порта (L3): memorycache проходит ту же suite, что и фейк. func TestCache_Contract(t *testing.T) { porttest.RunCacheContract(t, func(t *testing.T) (port.Cache[[]domain.Post], func(time.Duration)) { clk := porttest.NewClock() c := memorycache.New[[]domain.Post](memorycache.Config{Max: 16, Now: clk.Now}) return c, clk.Advance }) } // TTL-переход fresh→stale БЕЗ удаления (основа SWR, AGENTS.md §2). func TestCache_TTLTransitionsToStale_KeepsEntry(t *testing.T) { c, clk := newWithClock(t, 4) c.Set("k", []domain.Post{{ID: "1"}}, time.Second) // в пределах TTL — fresh if _, fresh, ok := c.Get("k"); !ok || !fresh { t.Fatalf("within TTL: ok=%v fresh=%v, want true/true", ok, fresh) } clk.Advance(2 * time.Second) // за пределы TTL got, fresh, ok := c.Get("k") if !ok { t.Fatal("entry must be RETAINED after TTL expiry (no timer-based deletion)") } if fresh { t.Fatal("entry must be stale after TTL expiry") } if len(got) != 1 || got[0].ID != "1" { t.Fatalf("stale value = %+v, want id=1", got) } } // LRU-вытеснение по recency: обращение поднимает запись, вытесняется хвост. func TestCache_LRUEviction(t *testing.T) { c, _ := newWithClock(t, 2) set := func(k string) { c.Set(k, []domain.Post{{ID: k}}, time.Minute) } set("a") // LRU: [a] set("b") // LRU: [b, a] // обратимся к "a" → она становится наиболее свежей: [a, b] if _, _, ok := c.Get("a"); !ok { t.Fatal("expected a present before eviction") } set("c") // превышение ёмкости → вытесняется хвост (b): [c, a] if _, _, ok := c.Get("b"); ok { t.Error("b should be evicted (least recently used)") } if _, _, ok := c.Get("a"); !ok { t.Error("a should remain (recently used)") } if _, _, ok := c.Get("c"); !ok { t.Error("c should remain (just inserted)") } } // Set на существующий ключ не увеличивает число записей (апсерт, не вставка). func TestCache_SetExistingDoesNotGrowSize(t *testing.T) { c, _ := newWithClock(t, 1) c.Set("k", []domain.Post{{ID: "1"}}, time.Minute) c.Set("k", []domain.Post{{ID: "2"}}, time.Minute) c.Set("k", []domain.Post{{ID: "3"}}, time.Minute) // при ёмкости 1 другая запись должна вытесниться, а сама k — остаться. if _, _, ok := c.Get("k"); !ok { t.Fatal("k must remain after repeated Set") } } // Конкурентный доступ — чисто под -race (AGENTS.md §6: memorycache — всегда -race). func TestCache_Concurrent(t *testing.T) { c, _ := newWithClock(t, 32) const goroutines = 200 var wg sync.WaitGroup wg.Add(goroutines) for i := 0; i < goroutines; i++ { go func(i int) { defer wg.Done() k := strconv.Itoa(i % 10) // горячие ключи — будет и апсерт, и коллизии c.Set(k, []domain.Post{{ID: k}}, time.Second) c.Get(k) }(i) } wg.Wait() }