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