| // Copyright 2024 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 ( |
| "fmt" |
| "internal/abi" |
| "internal/asan" |
| "internal/msan" |
| "internal/race" |
| "internal/testenv" |
| "math/rand/v2" |
| "reflect" |
| "runtime" |
| "strconv" |
| "strings" |
| "testing" |
| "time" |
| "unsafe" |
| ) |
| |
| // Set up special types. Because the internal maps are sharded by type, |
| // this will ensure that we're not overlapping with other tests. |
| type testString string |
| type testIntArray [4]int |
| type testEface any |
| type testStringArray [3]string |
| type testStringStruct struct { |
| a string |
| } |
| type testStringStructArrayStruct struct { |
| s [2]testStringStruct |
| } |
| type testStruct struct { |
| z float64 |
| b string |
| } |
| type testZeroSize struct{} |
| type testNestedHandle struct { |
| next Handle[testNestedHandle] |
| arr [6]int |
| } |
| |
| func TestHandle(t *testing.T) { |
| testHandle(t, testString("foo")) |
| testHandle(t, testString("bar")) |
| testHandle(t, testString("")) |
| testHandle(t, testIntArray{7, 77, 777, 7777}) |
| testHandle(t, testEface(nil)) |
| testHandle(t, testStringArray{"a", "b", "c"}) |
| testHandle(t, testStringStruct{"x"}) |
| testHandle(t, testStringStructArrayStruct{ |
| s: [2]testStringStruct{{"y"}, {"z"}}, |
| }) |
| testHandle(t, testStruct{0.5, "184"}) |
| testHandle(t, testEface("hello")) |
| testHandle(t, testZeroSize(struct{}{})) |
| } |
| |
| func testHandle[T comparable](t *testing.T, value T) { |
| name := reflect.TypeFor[T]().Name() |
| t.Run(fmt.Sprintf("%s/%#v", name, value), func(t *testing.T) { |
| v0 := Make(value) |
| v1 := Make(value) |
| |
| if v0.Value() != v1.Value() { |
| t.Error("v0.Value != v1.Value") |
| } |
| if v0.Value() != value { |
| t.Errorf("v0.Value not %#v", value) |
| } |
| if v0 != v1 { |
| t.Error("v0 != v1") |
| } |
| |
| drainMaps[T](t) |
| checkMapsFor(t, value) |
| }) |
| } |
| |
| // drainMaps ensures that the internal maps are drained. |
| func drainMaps[T comparable](t *testing.T) { |
| t.Helper() |
| |
| if unsafe.Sizeof(*(new(T))) == 0 { |
| return // zero-size types are not inserted. |
| } |
| drainCleanupQueue(t) |
| } |
| |
| func drainCleanupQueue(t *testing.T) { |
| t.Helper() |
| |
| runtime.GC() // Queue up the cleanups. |
| runtime_blockUntilEmptyCleanupQueue(int64(5 * time.Second)) |
| } |
| |
| func checkMapsFor[T comparable](t *testing.T, value T) { |
| // Manually load the value out of the map. |
| typ := abi.TypeFor[T]() |
| a, ok := uniqueMaps.Load(typ) |
| if !ok { |
| return |
| } |
| m := a.(*uniqueMap[T]) |
| p := m.Load(value) |
| if p != nil { |
| t.Errorf("value %v still referenced by a handle (or tiny block?): internal pointer %p", value, p) |
| } |
| } |
| |
| func TestMakeClonesStrings(t *testing.T) { |
| s := strings.Clone("abcdefghijklmnopqrstuvwxyz") // N.B. Must be big enough to not be tiny-allocated. |
| ran := make(chan bool) |
| runtime.AddCleanup(unsafe.StringData(s), func(ch chan bool) { |
| ch <- true |
| }, ran) |
| h := Make(s) |
| |
| // Clean up s (hopefully) and run the cleanup. |
| runtime.GC() |
| |
| select { |
| case <-time.After(1 * time.Second): |
| t.Fatal("string was improperly retained") |
| case <-ran: |
| } |
| runtime.KeepAlive(h) |
| } |
| |
| func TestHandleUnsafeString(t *testing.T) { |
| var testData []string |
| for i := range 1024 { |
| testData = append(testData, strconv.Itoa(i)) |
| } |
| var buf []byte |
| var handles []Handle[string] |
| for _, s := range testData { |
| if len(buf) < len(s) { |
| buf = make([]byte, len(s)*2) |
| } |
| copy(buf, s) |
| sbuf := unsafe.String(&buf[0], len(s)) |
| handles = append(handles, Make(sbuf)) |
| } |
| for i, s := range testData { |
| h := Make(s) |
| if handles[i].Value() != h.Value() { |
| t.Fatal("unsafe string improperly retained internally") |
| } |
| } |
| } |
| |
| func nestHandle(n testNestedHandle) testNestedHandle { |
| return testNestedHandle{ |
| next: Make(n), |
| arr: n.arr, |
| } |
| } |
| |
| func TestNestedHandle(t *testing.T) { |
| n0 := testNestedHandle{arr: [6]int{1, 2, 3, 4, 5, 6}} |
| n1 := nestHandle(n0) |
| n2 := nestHandle(n1) |
| n3 := nestHandle(n2) |
| |
| if v := n3.next.Value(); v != n2 { |
| t.Errorf("n3.Value != n2: %#v vs. %#v", v, n2) |
| } |
| if v := n2.next.Value(); v != n1 { |
| t.Errorf("n2.Value != n1: %#v vs. %#v", v, n1) |
| } |
| if v := n1.next.Value(); v != n0 { |
| t.Errorf("n1.Value != n0: %#v vs. %#v", v, n0) |
| } |
| |
| // In a good implementation, the entire chain, down to the bottom-most |
| // value, should all be gone after we drain the maps. |
| drainMaps[testNestedHandle](t) |
| checkMapsFor(t, n0) |
| } |
| |
| // Implemented in runtime. |
| // |
| // Used only by tests. |
| // |
| //go:linkname runtime_blockUntilEmptyCleanupQueue |
| func runtime_blockUntilEmptyCleanupQueue(timeout int64) bool |
| |
| var ( |
| randomNumber = rand.IntN(1000000) + 1000000 |
| heapBytes = newHeapBytes() |
| heapString = newHeapString() |
| |
| stringHandle Handle[string] |
| intHandle Handle[int] |
| anyHandle Handle[any] |
| pairHandle Handle[[2]string] |
| ) |
| |
| func TestMakeAllocs(t *testing.T) { |
| errorf := t.Errorf |
| if race.Enabled || msan.Enabled || asan.Enabled || testenv.OptimizationOff() { |
| errorf = t.Logf |
| } |
| |
| tests := []struct { |
| name string |
| allocs int |
| f func() |
| }{ |
| {name: "create heap bytes", allocs: 1, f: func() { |
| heapBytes = newHeapBytes() |
| }}, |
| |
| {name: "create heap string", allocs: 2, f: func() { |
| heapString = newHeapString() |
| }}, |
| |
| {name: "static string", allocs: 0, f: func() { |
| stringHandle = Make("this string is statically allocated") |
| }}, |
| |
| {name: "heap string", allocs: 0, f: func() { |
| stringHandle = Make(heapString) |
| }}, |
| |
| {name: "stack string", allocs: 0, f: func() { |
| var b [16]byte |
| b[8] = 'a' |
| stringHandle = Make(string(b[:])) |
| }}, |
| |
| {name: "bytes", allocs: 0, f: func() { |
| stringHandle = Make(string(heapBytes)) |
| }}, |
| |
| {name: "bytes truncated short", allocs: 0, f: func() { |
| stringHandle = Make(string(heapBytes[:16])) |
| }}, |
| |
| {name: "bytes truncated long", allocs: 0, f: func() { |
| stringHandle = Make(string(heapBytes[:40])) |
| }}, |
| |
| {name: "string to any", allocs: 1, f: func() { |
| anyHandle = Make[any](heapString) |
| }}, |
| |
| {name: "large number", allocs: 0, f: func() { |
| intHandle = Make(randomNumber) |
| }}, |
| |
| {name: "large number to any", allocs: 1, f: func() { |
| anyHandle = Make[any](randomNumber) |
| }}, |
| |
| {name: "pair", allocs: 0, f: func() { |
| pairHandle = Make([2]string{heapString, heapString}) |
| }}, |
| |
| {name: "pair from stack", allocs: 0, f: func() { |
| var b [16]byte |
| b[8] = 'a' |
| pairHandle = Make([2]string{string(b[:]), string(b[:])}) |
| }}, |
| |
| {name: "pair to any", allocs: 1, f: func() { |
| anyHandle = Make[any]([2]string{heapString, heapString}) |
| }}, |
| } |
| |
| for _, tt := range tests { |
| allocs := testing.AllocsPerRun(100, tt.f) |
| if allocs != float64(tt.allocs) { |
| errorf("%s: got %v allocs, want %v", tt.name, allocs, tt.allocs) |
| } |
| } |
| } |
| |
| //go:noinline |
| func newHeapBytes() []byte { |
| const N = 100 |
| b := make([]byte, N) |
| for i := range b { |
| b[i] = byte(i) |
| } |
| return b |
| } |
| |
| //go:noinline |
| func newHeapString() string { |
| return string(newHeapBytes()) |
| } |