mirror of
https://github.com/coder/coder.git
synced 2026-09-22 13:10:21 +08:00
290 lines
6.2 KiB
Go
290 lines
6.2 KiB
Go
package syncmap_test
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/util/syncmap"
|
|
)
|
|
|
|
// The tests below pin Map to the sync.Map contract it wraps. Where the
|
|
// stdlib returns a value, Map must return that same value typed as V,
|
|
// and where the stdlib returns nil, Map must return the zero V.
|
|
|
|
func TestStoreLoad(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, int]()
|
|
|
|
v, ok := m.Load("missing")
|
|
require.False(t, ok)
|
|
require.Zero(t, v)
|
|
|
|
m.Store("key", 1)
|
|
v, ok = m.Load("key")
|
|
require.True(t, ok)
|
|
require.Equal(t, 1, v)
|
|
|
|
m.Store("key", 2)
|
|
v, ok = m.Load("key")
|
|
require.True(t, ok)
|
|
require.Equal(t, 2, v)
|
|
}
|
|
|
|
func TestLoadOrStore(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, int]()
|
|
|
|
actual, loaded := m.LoadOrStore("key", 1)
|
|
require.False(t, loaded)
|
|
require.Equal(t, 1, actual, "stored value must be returned, not the zero value")
|
|
|
|
actual, loaded = m.LoadOrStore("key", 2)
|
|
require.True(t, loaded)
|
|
require.Equal(t, 1, actual, "existing value must win")
|
|
|
|
v, ok := m.Load("key")
|
|
require.True(t, ok)
|
|
require.Equal(t, 1, v)
|
|
}
|
|
|
|
// TestLoadOrStorePointer covers the load-or-create pattern, where a
|
|
// zero-value return is a nil pointer the caller then dereferences.
|
|
func TestLoadOrStorePointer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, *atomic.Int32]()
|
|
|
|
for range 3 {
|
|
counter, _ := m.LoadOrStore("key", &atomic.Int32{})
|
|
require.NotNil(t, counter)
|
|
counter.Add(1)
|
|
}
|
|
|
|
counter, ok := m.Load("key")
|
|
require.True(t, ok)
|
|
require.Equal(t, int32(3), counter.Load(), "all callers must share one counter")
|
|
}
|
|
|
|
func TestLoadOrStoreConcurrent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const goroutines = 16
|
|
|
|
m := syncmap.New[string, *atomic.Int32]()
|
|
|
|
var start, done sync.WaitGroup
|
|
start.Add(1)
|
|
done.Add(goroutines)
|
|
winners := make([]*atomic.Int32, goroutines)
|
|
loadedFlags := make([]bool, goroutines)
|
|
for i := range goroutines {
|
|
go func() {
|
|
defer done.Done()
|
|
start.Wait()
|
|
winners[i], loadedFlags[i] = m.LoadOrStore("key", &atomic.Int32{})
|
|
}()
|
|
}
|
|
start.Done()
|
|
done.Wait()
|
|
|
|
stored, ok := m.Load("key")
|
|
require.True(t, ok)
|
|
stores := 0
|
|
for i, winner := range winners {
|
|
require.Same(t, stored, winner, "goroutine %d observed a different value than the map holds", i)
|
|
if !loadedFlags[i] {
|
|
stores++
|
|
}
|
|
}
|
|
require.Equal(t, 1, stores, "exactly one goroutine should store")
|
|
}
|
|
|
|
func TestLoadAndDelete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, int]()
|
|
|
|
actual, loaded := m.LoadAndDelete("missing")
|
|
require.False(t, loaded)
|
|
require.Zero(t, actual)
|
|
|
|
m.Store("key", 1)
|
|
actual, loaded = m.LoadAndDelete("key")
|
|
require.True(t, loaded)
|
|
require.Equal(t, 1, actual)
|
|
|
|
_, ok := m.Load("key")
|
|
require.False(t, ok)
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, int]()
|
|
|
|
m.Delete("missing") // No-op.
|
|
|
|
m.Store("key", 1)
|
|
m.Delete("key")
|
|
_, ok := m.Load("key")
|
|
require.False(t, ok)
|
|
}
|
|
|
|
func TestSwap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, int]()
|
|
|
|
previous, loaded := m.Swap("key", 1)
|
|
require.False(t, loaded)
|
|
require.Zero(t, previous)
|
|
|
|
previous, loaded = m.Swap("key", 2)
|
|
require.True(t, loaded)
|
|
require.Equal(t, 1, previous)
|
|
|
|
v, ok := m.Load("key")
|
|
require.True(t, ok)
|
|
require.Equal(t, 2, v)
|
|
}
|
|
|
|
// TestSwapTyped pins previous to V rather than any: dereferencing it
|
|
// only compiles if the wrapper returns the value type.
|
|
func TestSwapTyped(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, *int]()
|
|
first, second := 1, 2
|
|
|
|
previous, loaded := m.Swap("key", &first)
|
|
require.False(t, loaded)
|
|
require.Nil(t, previous)
|
|
|
|
previous, loaded = m.Swap("key", &second)
|
|
require.True(t, loaded)
|
|
require.Equal(t, 1, *previous)
|
|
}
|
|
|
|
func TestCompareAndSwap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, int]()
|
|
|
|
require.False(t, m.CompareAndSwap("missing", 1, 2))
|
|
|
|
m.Store("key", 1)
|
|
require.False(t, m.CompareAndSwap("key", 2, 3), "swap must not happen on mismatch")
|
|
require.True(t, m.CompareAndSwap("key", 1, 3))
|
|
|
|
v, ok := m.Load("key")
|
|
require.True(t, ok)
|
|
require.Equal(t, 3, v)
|
|
}
|
|
|
|
func TestCompareAndDelete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, int]()
|
|
|
|
require.False(t, m.CompareAndDelete("missing", 1))
|
|
|
|
m.Store("key", 1)
|
|
require.False(t, m.CompareAndDelete("key", 2), "delete must not happen on mismatch")
|
|
require.True(t, m.CompareAndDelete("key", 1))
|
|
|
|
_, ok := m.Load("key")
|
|
require.False(t, ok)
|
|
}
|
|
|
|
func TestRange(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, int]()
|
|
want := map[string]int{"a": 1, "b": 2, "c": 3}
|
|
for k, v := range want {
|
|
m.Store(k, v)
|
|
}
|
|
|
|
got := make(map[string]int)
|
|
m.Range(func(key string, value int) bool {
|
|
got[key] = value
|
|
return true
|
|
})
|
|
require.Equal(t, want, got)
|
|
|
|
visited := 0
|
|
m.Range(func(string, int) bool {
|
|
visited++
|
|
return false
|
|
})
|
|
require.Equal(t, 1, visited, "returning false must stop iteration")
|
|
}
|
|
|
|
// TestNilInterfaceValue covers an interface value type holding nil.
|
|
// sync.Map stores it as a nil `any`, which cannot be type-asserted, so
|
|
// every read path has to return the zero V instead of panicking.
|
|
func TestNilInterfaceValue(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var nilErr error
|
|
|
|
t.Run("LoadOrStore", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, error]()
|
|
actual, loaded := m.LoadOrStore("key", nilErr)
|
|
require.False(t, loaded)
|
|
require.NoError(t, actual)
|
|
})
|
|
|
|
t.Run("Load", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, error]()
|
|
m.Store("key", nilErr)
|
|
v, ok := m.Load("key")
|
|
require.True(t, ok, "a stored nil is still a present key")
|
|
require.NoError(t, v)
|
|
})
|
|
|
|
t.Run("LoadAndDelete", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, error]()
|
|
m.Store("key", nilErr)
|
|
v, loaded := m.LoadAndDelete("key")
|
|
require.True(t, loaded)
|
|
require.NoError(t, v)
|
|
})
|
|
|
|
t.Run("Swap", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, error]()
|
|
m.Store("key", nilErr)
|
|
previous, loaded := m.Swap("key", nilErr)
|
|
require.True(t, loaded)
|
|
require.NoError(t, previous)
|
|
})
|
|
|
|
t.Run("Range", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := syncmap.New[string, error]()
|
|
m.Store("key", nilErr)
|
|
visited := 0
|
|
m.Range(func(key string, value error) bool {
|
|
visited++
|
|
require.Equal(t, "key", key)
|
|
require.NoError(t, value)
|
|
return true
|
|
})
|
|
require.Equal(t, 1, visited)
|
|
})
|
|
}
|