| package resultcache |
|
|
| import ( |
| "context" |
| "errors" |
| "sync" |
| "sync/atomic" |
| "testing" |
| "time" |
| ) |
|
|
| func TestCacheExpiresAndEvictsOldestEntry(t *testing.T) { |
| now := time.Date(2026, 7, 13, 0, 0, 0, 0, time.UTC) |
| cache := New[string, int](2, time.Minute) |
| cache.Set("first", 1, now) |
| cache.Set("second", 2, now.Add(time.Second)) |
| cache.Set("third", 3, now.Add(2*time.Second)) |
|
|
| if _, ok := cache.Get("first", now.Add(3*time.Second)); ok { |
| t.Fatal("oldest cache entry was not evicted") |
| } |
| if value, ok := cache.Get("third", now.Add(3*time.Second)); !ok || value != 3 { |
| t.Fatalf("third entry = %d, %v", value, ok) |
| } |
| if _, ok := cache.Get("second", now.Add(2*time.Minute)); ok { |
| t.Fatal("expired cache entry was returned") |
| } |
| } |
|
|
| func TestCacheDeleteInvalidatesStoredValue(t *testing.T) { |
| now := time.Date(2026, 7, 18, 0, 0, 0, 0, time.UTC) |
| cache := New[string, int](2, time.Minute) |
| cache.Set("account-risk", 1, now) |
| cache.Delete("account-risk") |
|
|
| if _, ok := cache.Get("account-risk", now); ok { |
| t.Fatal("deleted cache entry was returned") |
| } |
| } |
|
|
| func TestCacheCoalescesConcurrentLoads(t *testing.T) { |
| now := time.Now() |
| cache := New[string, int](2, time.Minute) |
| var calls atomic.Int32 |
| start := make(chan struct{}) |
| var wait sync.WaitGroup |
| results := make([]int, 2) |
| wait.Add(2) |
| for index := range results { |
| go func() { |
| defer wait.Done() |
| <-start |
| value, err := cache.Load(context.Background(), "shared", now, func() (int, error) { |
| calls.Add(1) |
| time.Sleep(10 * time.Millisecond) |
| return 42, nil |
| }) |
| if err != nil { |
| t.Errorf("load: %v", err) |
| } |
| results[index] = value |
| }() |
| } |
| close(start) |
| wait.Wait() |
| if calls.Load() != 1 || results[0] != 42 || results[1] != 42 { |
| t.Fatalf("calls = %d, results = %#v", calls.Load(), results) |
| } |
| } |
|
|
| func TestCacheWaiterHonorsCancellation(t *testing.T) { |
| cache := New[string, int](2, time.Minute) |
| loaderStarted := make(chan struct{}) |
| releaseLoader := make(chan struct{}) |
| loaderDone := make(chan struct{}) |
| go func() { |
| defer close(loaderDone) |
| _, _ = cache.Load(context.Background(), "shared", time.Now(), func() (int, error) { |
| close(loaderStarted) |
| <-releaseLoader |
| return 42, nil |
| }) |
| }() |
| <-loaderStarted |
|
|
| ctx, cancel := context.WithCancel(context.Background()) |
| cancel() |
| if _, err := cache.Load(ctx, "shared", time.Now(), func() (int, error) { |
| t.Fatal("canceled waiter unexpectedly became the loader") |
| return 0, nil |
| }); !errors.Is(err, context.Canceled) { |
| t.Fatalf("load error = %v, want context canceled", err) |
| } |
|
|
| close(releaseLoader) |
| <-loaderDone |
| } |
|
|