blob: e8f56d8e000a235deaa0d1c8604c5875aa22a1ff [file] [log] [blame]
// 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 unique
import (
"internal/abi"
"runtime"
"strconv"
"sync"
"testing"
"unsafe"
)
func TestCanonMap(t *testing.T) {
testCanonMap(t, func() *canonMap[string] {
return newCanonMap[string]()
})
}
func TestCanonMapBadHash(t *testing.T) {
testCanonMap(t, func() *canonMap[string] {
return newBadCanonMap[string]()
})
}
func TestCanonMapTruncHash(t *testing.T) {
testCanonMap(t, func() *canonMap[string] {
// Stub out the good hash function with a different terrible one
// (truncated hash). Everything should still work as expected.
// This is useful to test independently to catch issues with
// near collisions, where only the last few bits of the hash differ.
return newTruncCanonMap[string]()
})
}
func testCanonMap(t *testing.T, newMap func() *canonMap[string]) {
t.Run("LoadEmpty", func(t *testing.T) {
m := newMap()
for _, s := range testData {
expectMissing(t, s)(m.Load(s))
}
})
t.Run("LoadOrStore", func(t *testing.T) {
t.Run("Sequential", func(t *testing.T) {
m := newMap()
var refs []*string
for _, s := range testData {
expectMissing(t, s)(m.Load(s))
refs = append(refs, expectPresent(t, s)(m.LoadOrStore(s)))
expectPresent(t, s)(m.Load(s))
expectPresent(t, s)(m.LoadOrStore(s))
}
drainCleanupQueue(t)
for _, s := range testData {
expectPresent(t, s)(m.Load(s))
expectPresent(t, s)(m.LoadOrStore(s))
}
runtime.KeepAlive(refs)
refs = nil
drainCleanupQueue(t)
for _, s := range testData {
expectMissing(t, s)(m.Load(s))
expectPresent(t, s)(m.LoadOrStore(s))
}
})
t.Run("ConcurrentUnsharedKeys", func(t *testing.T) {
makeKey := func(s string, id int) string {
return s + "-" + strconv.Itoa(id)
}
// Expand and shrink the map multiple times to try to get
// insertions and cleanups to overlap.
m := newMap()
gmp := runtime.GOMAXPROCS(-1)
for try := range 3 {
var wg sync.WaitGroup
for i := range gmp {
wg.Add(1)
go func(id int) {
defer wg.Done()
var refs []*string
for _, s := range testData {
key := makeKey(s, id)
if try == 0 {
expectMissing(t, key)(m.Load(key))
}
refs = append(refs, expectPresent(t, key)(m.LoadOrStore(key)))
expectPresent(t, key)(m.Load(key))
expectPresent(t, key)(m.LoadOrStore(key))
}
for i, s := range testData {
key := makeKey(s, id)
expectPresent(t, key)(m.Load(key))
if got, want := expectPresent(t, key)(m.LoadOrStore(key)), refs[i]; got != want {
t.Errorf("canonical entry %p did not match ref %p", got, want)
}
}
// N.B. We avoid trying to test entry cleanup here
// because it's going to be very flaky, especially
// in the bad hash cases.
}(i)
}
wg.Wait()
}
// Drain cleanups so everything is deleted.
drainCleanupQueue(t)
// Double-check that it's all gone.
for id := range gmp {
makeKey := func(s string) string {
return s + "-" + strconv.Itoa(id)
}
for _, s := range testData {
key := makeKey(s)
expectMissing(t, key)(m.Load(key))
}
}
})
})
}
func expectMissing[T comparable](t *testing.T, key T) func(got *T) {
t.Helper()
return func(got *T) {
t.Helper()
if got != nil {
t.Errorf("expected key %v to be missing from map, got %p", key, got)
}
}
}
func expectPresent[T comparable](t *testing.T, key T) func(got *T) *T {
t.Helper()
return func(got *T) *T {
t.Helper()
if got == nil {
t.Errorf("expected key %v to be present in map, got %p", key, got)
}
if got != nil && *got != key {
t.Errorf("key %v is present in map, but canonical version has the wrong value: got %v, want %v", key, *got, key)
}
return got
}
}
// newBadCanonMap creates a new canonMap for the provided key type
// but with an intentionally bad hash function.
func newBadCanonMap[T comparable]() *canonMap[T] {
// Stub out the good hash function with a terrible one.
// Everything should still work as expected.
m := newCanonMap[T]()
m.hash = func(_ unsafe.Pointer, _ uintptr) uintptr {
return 0
}
return m
}
// newTruncCanonMap creates a new canonMap for the provided key type
// but with an intentionally bad hash function.
func newTruncCanonMap[T comparable]() *canonMap[T] {
// Stub out the good hash function with a terrible one.
// Everything should still work as expected.
m := newCanonMap[T]()
var mx map[string]int
mapType := abi.TypeOf(mx).MapType()
hasher := mapType.Hasher
m.hash = func(p unsafe.Pointer, n uintptr) uintptr {
return hasher(p, n) & ((uintptr(1) << 4) - 1)
}
return m
}