Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: go build -v ./...

- name: Test
run: go test -v -cover ./... -coverprofile coverage.out -coverpkg ./...
run: go test -race -v -cover ./... -coverprofile coverage.out -coverpkg ./...

- name: Upload coverage to Codecov
run: bash <(curl -s https://codecov.io/bash)
108 changes: 108 additions & 0 deletions safe_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package maps

import (
"runtime"
"testing"
)

var emptyBenchValue int
var emptyBenchFound bool

// BenchmarkEmptyLoad distinguishes nil backing storage from an allocated empty map.
func BenchmarkEmptyLoad(b *testing.B) {
for _, state := range []string{"Zero", "Cleared", "Allocated", "Populated"} {
b.Run(state, func(b *testing.B) {
m := new(SafeMap[string, int])
switch state {
case "Cleared":
m.Set("key", 1)
m.Clear()
case "Allocated":
m.Set("key", 1)
m.Delete("key")
case "Populated":
m.Set("key", 1)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
emptyBenchValue, emptyBenchFound = m.Load("key")
}
})
}
}

func BenchmarkZeroLen(b *testing.B) {
m := new(SafeMap[string, int])
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
emptyBenchValue = m.Len()
}
}

func BenchmarkZeroClear(b *testing.B) {
m := new(SafeMap[string, int])
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.Clear()
}
}

func BenchmarkZeroLoadParallel(b *testing.B) {
m := new(SafeMap[string, int])
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
var v int
var ok bool
for pb.Next() {
v, ok = m.Load("missing")
}
runtime.KeepAlive(v)
runtime.KeepAlive(ok)
})
}

func BenchmarkPopulatedStore(b *testing.B) {
m := new(SafeMap[string, int])
m.Set("key", 1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.Set("key", i)
}
}

func BenchmarkSetClearCycle(b *testing.B) {
m := new(SafeMap[string, int])
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.Set("key", i)
m.Clear()
}
}

func BenchmarkSliceZeroRange(b *testing.B) {
m := new(SafeSliceMap[string, int])
yield := func(string, int) bool { return true }
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.Range(yield)
}
}

func BenchmarkSliceZeroRangeParallel(b *testing.B) {
m := new(SafeSliceMap[string, int])
yield := func(string, int) bool { return true }
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.Range(yield)
}
})
}
107 changes: 107 additions & 0 deletions safe_concurrency_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package maps

import (
"encoding/json"
"sync"
"testing"
)

func TestSafeMapsConcurrentAccess(t *testing.T) {
constructors := []struct {
name string
new func() MapI[string, int]
}{
{"SafeMap", func() MapI[string, int] { return new(SafeMap[string, int]) }},
{"SafeSliceMap", func() MapI[string, int] { return new(SafeSliceMap[string, int]) }},
}
operations := []struct {
name string
run func(MapI[string, int])
}{
{"Load", func(m MapI[string, int]) { m.Load("key") }},
{"Get", func(m MapI[string, int]) { m.Get("key") }},
{"Has", func(m MapI[string, int]) { m.Has("key") }},
{"Keys", func(m MapI[string, int]) { m.Keys() }},
{"Values", func(m MapI[string, int]) { m.Values() }},
{"Len", func(m MapI[string, int]) { m.Len() }},
{"Range", func(m MapI[string, int]) {
m.Range(func(string, int) bool { return true })
}},
{"All", func(m MapI[string, int]) {
for range m.All() {
}
}},
{"KeysIter", func(m MapI[string, int]) {
for range m.KeysIter() {
}
}},
{"ValuesIter", func(m MapI[string, int]) {
for range m.ValuesIter() {
}
}},
{"Clear", func(m MapI[string, int]) { m.Clear() }},
{"Copy", func(m MapI[string, int]) { m.Copy(StdMap[string, int]{"key": 1}) }},
{"UnmarshalJSON", func(m MapI[string, int]) {
_ = m.(json.Unmarshaler).UnmarshalJSON([]byte(`{"key":1}`))
}},
{"UnmarshalNull", func(m MapI[string, int]) {
_ = m.(json.Unmarshaler).UnmarshalJSON([]byte(`null`))
}},
}

for _, constructor := range constructors {
t.Run(constructor.name, func(t *testing.T) {
for _, operation := range operations {
t.Run(operation.name, func(t *testing.T) {
m := constructor.new()
start := make(chan struct{})
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
<-start
for range 256 {
operation.run(m)
}
}()
go func() {
defer wg.Done()
<-start
for range 256 {
m.Set("key", 1)
m.Clear()
}
}()
close(start)
wg.Wait()

// The map must remain usable after concurrent clearing and initialization.
m.Clear()
m.Copy(StdMap[string, int]{"key": 1})
if value, ok := m.Load("key"); !ok || value != 1 {
t.Fatalf("Load(key) = (%d, %t), want (1, true)", value, ok)
}
})
}
})
}
}

func TestSafeMapsNilIteration(t *testing.T) {
var m *SafeMap[string, int]
var sm *SafeSliceMap[string, int]
unexpected := func(string, int) bool {
t.Fatal("nil map must not invoke the callback")
return false
}
m.Range(unexpected)
m.All()(unexpected)
sm.Range(unexpected)
sm.All()(unexpected)
for range sm.KeysIter() {
t.Fatal("nil map must not yield keys")
}
for range sm.ValuesIter() {
t.Fatal("nil map must not yield values")
}
}
162 changes: 162 additions & 0 deletions safe_initialization_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package maps

import (
"encoding"
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func assertSafeMapContents(t *testing.T, m MapI[string, int], want map[string]int) {
t.Helper()
assert.Equal(t, len(want), m.Len())
for key, value := range want {
got, ok := m.Load(key)
assert.True(t, ok)
assert.Equal(t, value, got)
assert.Equal(t, value, m.Get(key))
assert.True(t, m.Has(key))
}
_, ok := m.Load("missing")
assert.False(t, ok)
got := make(map[string]int)
m.Range(func(k string, v int) bool { got[k] = v; return true })
assert.Equal(t, want, got)
got = make(map[string]int)
for k, v := range m.All() {
got[k] = v
}
assert.Equal(t, want, got)
var keys []string
var values []int
for k := range m.KeysIter() {
keys = append(keys, k)
}
for v := range m.ValuesIter() {
values = append(values, v)
}
expected := StdMap[string, int](want)
assert.ElementsMatch(t, expected.Keys(), keys)
assert.ElementsMatch(t, expected.Values(), values)
assert.ElementsMatch(t, expected.Keys(), m.Keys())
assert.ElementsMatch(t, expected.Values(), m.Values())
}

func TestSafeMapsInitialization(t *testing.T) {
for _, constructor := range []struct {
name string
new func() MapI[string, int]
}{
{"SafeMap", func() MapI[string, int] { return new(SafeMap[string, int]) }},
{"SafeSliceMap", func() MapI[string, int] { return new(SafeSliceMap[string, int]) }},
} {
t.Run(constructor.name, func(t *testing.T) {
source := constructor.new()
source.Set("key", 1)
binary, err := source.(encoding.BinaryMarshaler).MarshalBinary()
require.NoError(t, err)
for _, populate := range []struct {
name string
run func(*testing.T, MapI[string, int])
}{
{"Set", func(t *testing.T, m MapI[string, int]) { m.Set("key", 1) }},
{"Copy", func(t *testing.T, m MapI[string, int]) { m.Copy(StdMap[string, int]{"key": 1}) }},
{"Merge", func(t *testing.T, m MapI[string, int]) { m.Merge(StdMap[string, int]{"key": 1}) }},
{"Insert", func(t *testing.T, m MapI[string, int]) {
m.Copy(StdMap[string, int]{})
m.Insert(StdMap[string, int]{"key": 1}.All())
}},
{"JSON", func(t *testing.T, m MapI[string, int]) {
require.NoError(t, m.(json.Unmarshaler).UnmarshalJSON([]byte(`{"key":1}`)))
}},
{"Binary", func(t *testing.T, m MapI[string, int]) {
require.NoError(t, m.(encoding.BinaryUnmarshaler).UnmarshalBinary(binary))
}},
} {
t.Run(populate.name, func(t *testing.T) {
m := constructor.new()
assertSafeMapContents(t, m, map[string]int{})
for range 2 {
populate.run(t, m)
assertSafeMapContents(t, m, map[string]int{"key": 1})
m.Clear()
assertSafeMapContents(t, m, map[string]int{})
}
})
}
})
}
}

func TestSafeMapsConstructedContents(t *testing.T) {
source := StdMap[string, int]{"key": 1}
for _, m := range []MapI[string, int]{
NewSafeMap[string, int](source),
NewSafeSliceMap[string, int](source),
NewSafeMap[string, int](source).Clone(),
NewSafeSliceMap[string, int](source).Clone(),
CollectSafeMap(source.All()),
CollectSafeSliceMap(source.All()),
} {
assertSafeMapContents(t, m, map[string]int{"key": 1})
m.Clear()
assertSafeMapContents(t, m, map[string]int{})
m.Set("key", 2)
assertSafeMapContents(t, m, map[string]int{"key": 2})
}
}

func TestSafeMapPartialDecode(t *testing.T) {
var m SafeMap[string, int]
err := m.UnmarshalJSON([]byte(`{"key":1,"bad":"not an int"}`))
require.Error(t, err)
assertSafeMapContents(t, &m, map[string]int{"key": 1, "bad": 0})
require.NoError(t, m.UnmarshalJSON([]byte(`null`)))
assertSafeMapContents(t, &m, map[string]int{})
m.Set("key", 2)
assertSafeMapContents(t, &m, map[string]int{"key": 2})
require.Error(t, m.UnmarshalBinary([]byte("invalid gob")))
assertSafeMapContents(t, &m, map[string]int{})
m.Set("key", 3)
assertSafeMapContents(t, &m, map[string]int{"key": 3})
}

func TestSafeSliceMapInitializationTransitions(t *testing.T) {
var m SafeSliceMap[string, int]
for range 2 {
m.SetAt(0, "key", 1)
assertSafeMapContents(t, &m, map[string]int{"key": 1})
m.Clear()
assertSafeMapContents(t, &m, map[string]int{})
}
m.Set("key", 2)
require.Error(t, m.UnmarshalJSON([]byte(`{"key":"not an int"}`)))
assertSafeMapContents(t, &m, map[string]int{"key": 2})
require.Error(t, m.UnmarshalBinary([]byte("invalid gob")))
assertSafeMapContents(t, &m, map[string]int{"key": 2})
require.NoError(t, m.UnmarshalJSON([]byte(`null`)))
assertSafeMapContents(t, &m, map[string]int{})
m.SetAt(0, "key", 3)
assertSafeMapContents(t, &m, map[string]int{"key": 3})
}

func TestSafeSliceMapInitializationAfterPanic(t *testing.T) {
for _, operation := range []struct {
name string
set func(*SafeSliceMap[any, int], any, int)
}{
{"Set", func(m *SafeSliceMap[any, int], key any, value int) { m.Set(key, value) }},
{"SetAt", func(m *SafeSliceMap[any, int], key any, value int) { m.SetAt(0, key, value) }},
} {
t.Run(operation.name, func(t *testing.T) {
var m SafeSliceMap[any, int]
assert.Panics(t, func() { operation.set(&m, []int{1}, 1) })
operation.set(&m, "key", 2)
got := make(map[any]int)
m.Range(func(key any, value int) bool { got[key] = value; return true })
assert.Equal(t, map[any]int{"key": 2}, got)
})
}
}
Loading
Loading