Files
orca/internal/cache/cache_test.go
T
Jon Chery b6d4db1a96 feat(P00): CLI cache layer (R-008) — orca_cache SQLite + cache CLI
internal/cache/ package with per-class TTLs (Get/Set/Invalidate);
wired into node/job/ns list read paths; orca cache show/invalidate CLI.
Tests: hit/miss/invalidate/TTL-expiry + bench <1ms hit.

---ci---
project: orca
phase: 00
milestone: v0.11
status: execute
---/ci---
2026-08-07 04:17:25 +00:00

228 lines
5.8 KiB
Go

package cache
import (
"errors"
"path/filepath"
"testing"
"time"
)
func openTestCache(t *testing.T) (*Cache, func()) {
t.Helper()
path := filepath.Join(t.TempDir(), "orca_cache.db")
c, err := Open(path)
if err != nil {
t.Fatalf("open cache: %v", err)
}
return c, func() { _ = c.Close() }
}
func TestCache_Hit(t *testing.T) {
c, cleanup := openTestCache(t)
defer cleanup()
want := []byte("hello-orca")
if err := c.Set("nodes", "list", want, 30*time.Second); err != nil {
t.Fatalf("set: %v", err)
}
got, inserted, err := c.Get("nodes", "list")
if err != nil {
t.Fatalf("get: %v", err)
}
if string(got) != string(want) {
t.Errorf("get value = %q, want %q", got, want)
}
if inserted.IsZero() {
t.Errorf("inserted time is zero")
}
}
func TestCache_Miss(t *testing.T) {
c, cleanup := openTestCache(t)
defer cleanup()
got, _, err := c.Get("nodes", "missing")
if !errors.Is(err, ErrCacheMiss) {
t.Fatalf("get miss: err = %v, want ErrCacheMiss", err)
}
if got != nil {
t.Errorf("get miss value = %v, want nil", got)
}
}
func TestCache_Invalidate(t *testing.T) {
c, cleanup := openTestCache(t)
defer cleanup()
if err := c.Set("nodes", "list", []byte("a"), 30*time.Second); err != nil {
t.Fatalf("set a: %v", err)
}
if err := c.Set("nodes", "other", []byte("b"), 30*time.Second); err != nil {
t.Fatalf("set b: %v", err)
}
if err := c.Set("jobs", "list", []byte("c"), 30*time.Second); err != nil {
t.Fatalf("set c: %v", err)
}
if err := c.Invalidate("nodes"); err != nil {
t.Fatalf("invalidate: %v", err)
}
if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) {
t.Errorf("nodes/list after invalidate: err = %v, want ErrCacheMiss", err)
}
if _, _, err := c.Get("nodes", "other"); !errors.Is(err, ErrCacheMiss) {
t.Errorf("nodes/other after invalidate: err = %v, want ErrCacheMiss", err)
}
if _, _, err := c.Get("jobs", "list"); err != nil {
t.Errorf("jobs/list after nodes invalidate: err = %v, want nil", err)
}
}
func TestCache_InvalidateKey(t *testing.T) {
c, cleanup := openTestCache(t)
defer cleanup()
if err := c.Set("nodes", "list", []byte("a"), 30*time.Second); err != nil {
t.Fatalf("set: %v", err)
}
if err := c.InvalidateKey("nodes", "list"); err != nil {
t.Fatalf("invalidate key: %v", err)
}
if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) {
t.Errorf("get after invalidate key: err = %v, want ErrCacheMiss", err)
}
}
func TestCache_TTLExpiry(t *testing.T) {
c, cleanup := openTestCache(t)
defer cleanup()
if err := c.Set("jobs", "list", []byte("stale"), 1*time.Millisecond); err != nil {
t.Fatalf("set: %v", err)
}
time.Sleep(10 * time.Millisecond)
if _, _, err := c.Get("jobs", "list"); !errors.Is(err, ErrCacheMiss) {
t.Errorf("get after ttl expiry: err = %v, want ErrCacheMiss", err)
}
}
func TestCache_TTLZeroNeverExpires(t *testing.T) {
c, cleanup := openTestCache(t)
defer cleanup()
if err := c.Set("namespaces", "list", []byte("forever"), 0); err != nil {
t.Fatalf("set: %v", err)
}
time.Sleep(5 * time.Millisecond)
got, _, err := c.Get("namespaces", "list")
if err != nil {
t.Fatalf("get ttl=0: %v", err)
}
if string(got) != "forever" {
t.Errorf("get ttl=0 value = %q, want %q", got, "forever")
}
}
func TestCache_Overwrite(t *testing.T) {
c, cleanup := openTestCache(t)
defer cleanup()
if err := c.Set("nodes", "list", []byte("v1"), 30*time.Second); err != nil {
t.Fatalf("set v1: %v", err)
}
if err := c.Set("nodes", "list", []byte("v2"), 30*time.Second); err != nil {
t.Fatalf("set v2: %v", err)
}
got, _, err := c.Get("nodes", "list")
if err != nil {
t.Fatalf("get: %v", err)
}
if string(got) != "v2" {
t.Errorf("get after overwrite = %q, want %q", got, "v2")
}
}
func TestCache_InvalidateAll(t *testing.T) {
c, cleanup := openTestCache(t)
defer cleanup()
_ = c.Set("nodes", "list", []byte("a"), 30*time.Second)
_ = c.Set("jobs", "list", []byte("b"), 30*time.Second)
if err := c.InvalidateAll(); err != nil {
t.Fatalf("invalidate all: %v", err)
}
if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) {
t.Errorf("nodes/list after invalidate-all: err = %v, want ErrCacheMiss", err)
}
if _, _, err := c.Get("jobs", "list"); !errors.Is(err, ErrCacheMiss) {
t.Errorf("jobs/list after invalidate-all: err = %v, want ErrCacheMiss", err)
}
}
func TestCache_Stats(t *testing.T) {
c, cleanup := openTestCache(t)
defer cleanup()
_ = c.Set("nodes", "list", []byte("aaaa"), 30*time.Second)
_ = c.Set("jobs", "list", []byte("bb"), 30*time.Second)
stats, err := c.Stats()
if err != nil {
t.Fatalf("stats: %v", err)
}
if len(stats) != 2 {
t.Fatalf("stats len = %d, want 2", len(stats))
}
var nodes, jobs *ClassStats
for i := range stats {
switch stats[i].Class {
case "nodes":
nodes = &stats[i]
case "jobs":
jobs = &stats[i]
}
}
if nodes == nil || nodes.Count != 1 || nodes.Bytes != 4 {
t.Errorf("nodes stats = %+v, want count=1 bytes=4", nodes)
}
if jobs == nil || jobs.Count != 1 || jobs.Bytes != 2 {
t.Errorf("jobs stats = %+v, want count=1 bytes=2", jobs)
}
}
func TestCache_OpenDefaultPath(t *testing.T) {
dir := t.TempDir()
t.Setenv("ORCA_HOME", dir)
c, err := Open("")
if err != nil {
t.Fatalf("open default path: %v", err)
}
defer c.Close()
if err := c.Set("nodes", "list", []byte("ok"), 0); err != nil {
t.Fatalf("set: %v", err)
}
got, _, err := c.Get("nodes", "list")
if err != nil {
t.Fatalf("get: %v", err)
}
if string(got) != "ok" {
t.Errorf("get = %q, want %q", got, "ok")
}
}
func BenchmarkCacheHit(b *testing.B) {
path := filepath.Join(b.TempDir(), "orca_cache.db")
c, err := Open(path)
if err != nil {
b.Fatalf("open: %v", err)
}
defer c.Close()
if err := c.Set("nodes", "list", []byte("bench"), 0); err != nil {
b.Fatalf("set: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, _, err := c.Get("nodes", "list"); err != nil {
b.Fatalf("get: %v", err)
}
}
}