| // 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 |
| } |