blob: 4c7f1f9752e4d276b1b81f2fcf34e20efcf641d3 [file] [log] [blame] [edit]
// 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())
}