| // Copyright 2025 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package fips140cache | |
| import ( | |
| "context" | |
| "errors" | |
| "runtime" | |
| "sync" | |
| "testing" | |
| "time" | |
| ) | |
| func TestCache(t *testing.T) { | |
| c := new(Cache[key, value]) | |
| checkTrue := func(*value) bool { return true } | |
| checkFalse := func(*value) bool { return false } | |
| newNotCalled := func() (*value, error) { | |
| t.Helper() | |
| t.Fatal("new called") | |
| return nil, nil | |
| } | |
| k1 := newKey() | |
| v1 := &value{} | |
| v, err := c.Get(k1, func() (*value, error) { return v1, nil }, checkTrue) | |
| expectValue(t, v, err, v1) | |
| // Cached value is returned if check is true. | |
| v, err = c.Get(k1, newNotCalled, checkTrue) | |
| expectValue(t, v, err, v1) | |
| // New value is returned and cached if check is false. | |
| v2 := &value{} | |
| v, err = c.Get(k1, func() (*value, error) { return v2, nil }, checkFalse) | |
| expectValue(t, v, err, v2) | |
| v, err = c.Get(k1, newNotCalled, checkTrue) | |
| expectValue(t, v, err, v2) | |
| expectMapSize(t, c, 1) | |
| // Cache is evicted when key becomes unreachable. | |
| waitUnreachable(t, &k1) | |
| expectMapSize(t, c, 0) | |
| // Value is not cached if new returns an error. | |
| k2 := newKey() | |
| err1 := errors.New("error") | |
| _, err = c.Get(k2, func() (*value, error) { return nil, err1 }, checkTrue) | |
| if err != err1 { | |
| t.Errorf("got %v, want %v", err, err1) | |
| } | |
| expectMapSize(t, c, 0) | |
| // Value is not replaced if check is false and new returns an error. | |
| v, err = c.Get(k2, func() (*value, error) { return v1, nil }, checkTrue) | |
| expectValue(t, v, err, v1) | |
| _, err = c.Get(k2, func() (*value, error) { return v2, err1 }, checkFalse) | |
| if err != err1 { | |
| t.Errorf("got %v, want %v", err, err1) | |
| } | |
| v, err = c.Get(k2, newNotCalled, checkTrue) | |
| expectValue(t, v, err, v1) | |
| expectMapSize(t, c, 1) | |
| // Cache is evicted for keys used only once. | |
| k3 := newKey() | |
| v, err = c.Get(k3, func() (*value, error) { return v1, nil }, checkTrue) | |
| expectValue(t, v, err, v1) | |
| expectMapSize(t, c, 2) | |
| waitUnreachable(t, &k2) | |
| waitUnreachable(t, &k3) | |
| expectMapSize(t, c, 0) | |
| // When two goroutines race, the returned value may be the new or old one, | |
| // but the map must shrink to 0. | |
| keys := make([]*key, 100) | |
| for i := range keys { | |
| keys[i] = newKey() | |
| v1, v2 := &value{}, &value{} | |
| start := make(chan struct{}) | |
| var wg sync.WaitGroup | |
| wg.Add(2) | |
| go func() { | |
| <-start | |
| v, err := c.Get(keys[i], func() (*value, error) { return v1, nil }, checkTrue) | |
| expectValue(t, v, err, v1, v2) | |
| wg.Done() | |
| }() | |
| go func() { | |
| <-start | |
| v, err := c.Get(keys[i], func() (*value, error) { return v2, nil }, checkTrue) | |
| expectValue(t, v, err, v1, v2) | |
| wg.Done() | |
| }() | |
| close(start) | |
| wg.Wait() | |
| v3 := &value{} | |
| v, err := c.Get(keys[i], func() (*value, error) { return v3, nil }, checkTrue) | |
| expectValue(t, v, err, v1, v2) | |
| } | |
| for i := range keys { | |
| waitUnreachable(t, &keys[i]) | |
| } | |
| expectMapSize(t, c, 0) | |
| } | |
| type key struct { | |
| _ *int | |
| } | |
| type value struct { | |
| _ *int | |
| } | |
| // newKey allocates a key value on the heap. | |
| // | |
| //go:noinline | |
| func newKey() *key { | |
| return &key{} | |
| } | |
| func expectValue(t *testing.T, v *value, err error, want ...*value) { | |
| t.Helper() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, w := range want { | |
| if v == w { | |
| return | |
| } | |
| } | |
| t.Errorf("got %p, want %p", v, want) | |
| } | |
| func expectMapSize(t *testing.T, c *Cache[key, value], want int) { | |
| t.Helper() | |
| var size int | |
| // Loop a few times because the AddCleanup might not be done yet. | |
| for range 10 { | |
| size = 0 | |
| c.m.Range(func(_, _ any) bool { | |
| size++ | |
| return true | |
| }) | |
| if size == want { | |
| return | |
| } | |
| time.Sleep(100 * time.Millisecond) | |
| } | |
| t.Errorf("got %d, want %d", size, want) | |
| } | |
| func waitUnreachable(t *testing.T, k **key) { | |
| ctx, cancel := context.WithCancel(t.Context()) | |
| defer cancel() | |
| runtime.AddCleanup(*k, func(_ *int) { cancel() }, nil) | |
| *k = nil | |
| for ctx.Err() == nil { | |
| runtime.GC() | |
| } | |
| if ctx.Err() != context.Canceled { | |
| t.Fatal(ctx.Err()) | |
| } | |
| } |