Source file src/unique/handle_test.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package unique
     6  
     7  import (
     8  	"fmt"
     9  	"internal/abi"
    10  	"internal/asan"
    11  	"internal/msan"
    12  	"internal/race"
    13  	"internal/testenv"
    14  	"math/rand/v2"
    15  	"reflect"
    16  	"runtime"
    17  	"strconv"
    18  	"strings"
    19  	"testing"
    20  	"time"
    21  	"unsafe"
    22  )
    23  
    24  // Set up special types. Because the internal maps are sharded by type,
    25  // this will ensure that we're not overlapping with other tests.
    26  type testString string
    27  type testIntArray [4]int
    28  type testEface any
    29  type testStringArray [3]string
    30  type testStringStruct struct {
    31  	a string
    32  }
    33  type testStringStructArrayStruct struct {
    34  	s [2]testStringStruct
    35  }
    36  type testStruct struct {
    37  	z float64
    38  	b string
    39  }
    40  type testZeroSize struct{}
    41  type testNestedHandle struct {
    42  	next Handle[testNestedHandle]
    43  	arr  [6]int
    44  }
    45  
    46  func TestHandle(t *testing.T) {
    47  	testHandle(t, testString("foo"))
    48  	testHandle(t, testString("bar"))
    49  	testHandle(t, testString(""))
    50  	testHandle(t, testIntArray{7, 77, 777, 7777})
    51  	testHandle(t, testEface(nil))
    52  	testHandle(t, testStringArray{"a", "b", "c"})
    53  	testHandle(t, testStringStruct{"x"})
    54  	testHandle(t, testStringStructArrayStruct{
    55  		s: [2]testStringStruct{{"y"}, {"z"}},
    56  	})
    57  	testHandle(t, testStruct{0.5, "184"})
    58  	testHandle(t, testEface("hello"))
    59  	testHandle(t, testZeroSize(struct{}{}))
    60  }
    61  
    62  func testHandle[T comparable](t *testing.T, value T) {
    63  	name := reflect.TypeFor[T]().Name()
    64  	t.Run(fmt.Sprintf("%s/%#v", name, value), func(t *testing.T) {
    65  		v0 := Make(value)
    66  		v1 := Make(value)
    67  
    68  		if v0.Value() != v1.Value() {
    69  			t.Error("v0.Value != v1.Value")
    70  		}
    71  		if v0.Value() != value {
    72  			t.Errorf("v0.Value not %#v", value)
    73  		}
    74  		if v0 != v1 {
    75  			t.Error("v0 != v1")
    76  		}
    77  
    78  		drainMaps[T](t)
    79  		checkMapsFor(t, value)
    80  	})
    81  }
    82  
    83  // drainMaps ensures that the internal maps are drained.
    84  func drainMaps[T comparable](t *testing.T) {
    85  	t.Helper()
    86  
    87  	if unsafe.Sizeof(*(new(T))) == 0 {
    88  		return // zero-size types are not inserted.
    89  	}
    90  	drainCleanupQueue(t)
    91  }
    92  
    93  func drainCleanupQueue(t *testing.T) {
    94  	t.Helper()
    95  
    96  	runtime.GC() // Queue up the cleanups.
    97  	runtime_blockUntilEmptyCleanupQueue(int64(5 * time.Second))
    98  }
    99  
   100  func checkMapsFor[T comparable](t *testing.T, value T) {
   101  	// Manually load the value out of the map.
   102  	typ := abi.TypeFor[T]()
   103  	a, ok := uniqueMaps.Load(typ)
   104  	if !ok {
   105  		return
   106  	}
   107  	m := a.(*uniqueMap[T])
   108  	p := m.Load(value)
   109  	if p != nil {
   110  		t.Errorf("value %v still referenced by a handle (or tiny block?): internal pointer %p", value, p)
   111  	}
   112  }
   113  
   114  func TestMakeClonesStrings(t *testing.T) {
   115  	s := strings.Clone("abcdefghijklmnopqrstuvwxyz") // N.B. Must be big enough to not be tiny-allocated.
   116  	ran := make(chan bool)
   117  	runtime.AddCleanup(unsafe.StringData(s), func(ch chan bool) {
   118  		ch <- true
   119  	}, ran)
   120  	h := Make(s)
   121  
   122  	// Clean up s (hopefully) and run the cleanup.
   123  	runtime.GC()
   124  
   125  	select {
   126  	case <-time.After(1 * time.Second):
   127  		t.Fatal("string was improperly retained")
   128  	case <-ran:
   129  	}
   130  	runtime.KeepAlive(h)
   131  }
   132  
   133  func TestHandleUnsafeString(t *testing.T) {
   134  	var testData []string
   135  	for i := range 1024 {
   136  		testData = append(testData, strconv.Itoa(i))
   137  	}
   138  	var buf []byte
   139  	var handles []Handle[string]
   140  	for _, s := range testData {
   141  		if len(buf) < len(s) {
   142  			buf = make([]byte, len(s)*2)
   143  		}
   144  		copy(buf, s)
   145  		sbuf := unsafe.String(&buf[0], len(s))
   146  		handles = append(handles, Make(sbuf))
   147  	}
   148  	for i, s := range testData {
   149  		h := Make(s)
   150  		if handles[i].Value() != h.Value() {
   151  			t.Fatal("unsafe string improperly retained internally")
   152  		}
   153  	}
   154  }
   155  
   156  func nestHandle(n testNestedHandle) testNestedHandle {
   157  	return testNestedHandle{
   158  		next: Make(n),
   159  		arr:  n.arr,
   160  	}
   161  }
   162  
   163  func TestNestedHandle(t *testing.T) {
   164  	n0 := testNestedHandle{arr: [6]int{1, 2, 3, 4, 5, 6}}
   165  	n1 := nestHandle(n0)
   166  	n2 := nestHandle(n1)
   167  	n3 := nestHandle(n2)
   168  
   169  	if v := n3.next.Value(); v != n2 {
   170  		t.Errorf("n3.Value != n2: %#v vs. %#v", v, n2)
   171  	}
   172  	if v := n2.next.Value(); v != n1 {
   173  		t.Errorf("n2.Value != n1: %#v vs. %#v", v, n1)
   174  	}
   175  	if v := n1.next.Value(); v != n0 {
   176  		t.Errorf("n1.Value != n0: %#v vs. %#v", v, n0)
   177  	}
   178  
   179  	// In a good implementation, the entire chain, down to the bottom-most
   180  	// value, should all be gone after we drain the maps.
   181  	drainMaps[testNestedHandle](t)
   182  	checkMapsFor(t, n0)
   183  }
   184  
   185  // Implemented in runtime.
   186  //
   187  // Used only by tests.
   188  //
   189  //go:linkname runtime_blockUntilEmptyCleanupQueue
   190  func runtime_blockUntilEmptyCleanupQueue(timeout int64) bool
   191  
   192  var (
   193  	randomNumber = rand.IntN(1000000) + 1000000
   194  	heapBytes    = newHeapBytes()
   195  	heapString   = newHeapString()
   196  
   197  	stringHandle Handle[string]
   198  	intHandle    Handle[int]
   199  	anyHandle    Handle[any]
   200  	pairHandle   Handle[[2]string]
   201  )
   202  
   203  func TestMakeAllocs(t *testing.T) {
   204  	errorf := t.Errorf
   205  	if race.Enabled || msan.Enabled || asan.Enabled || testenv.OptimizationOff() {
   206  		errorf = t.Logf
   207  	}
   208  
   209  	tests := []struct {
   210  		name   string
   211  		allocs int
   212  		f      func()
   213  	}{
   214  		{name: "create heap bytes", allocs: 1, f: func() {
   215  			heapBytes = newHeapBytes()
   216  		}},
   217  
   218  		{name: "create heap string", allocs: 2, f: func() {
   219  			heapString = newHeapString()
   220  		}},
   221  
   222  		{name: "static string", allocs: 0, f: func() {
   223  			stringHandle = Make("this string is statically allocated")
   224  		}},
   225  
   226  		{name: "heap string", allocs: 0, f: func() {
   227  			stringHandle = Make(heapString)
   228  		}},
   229  
   230  		{name: "stack string", allocs: 0, f: func() {
   231  			var b [16]byte
   232  			b[8] = 'a'
   233  			stringHandle = Make(string(b[:]))
   234  		}},
   235  
   236  		{name: "bytes", allocs: 0, f: func() {
   237  			stringHandle = Make(string(heapBytes))
   238  		}},
   239  
   240  		{name: "bytes truncated short", allocs: 0, f: func() {
   241  			stringHandle = Make(string(heapBytes[:16]))
   242  		}},
   243  
   244  		{name: "bytes truncated long", allocs: 0, f: func() {
   245  			stringHandle = Make(string(heapBytes[:40]))
   246  		}},
   247  
   248  		{name: "string to any", allocs: 1, f: func() {
   249  			anyHandle = Make[any](heapString)
   250  		}},
   251  
   252  		{name: "large number", allocs: 0, f: func() {
   253  			intHandle = Make(randomNumber)
   254  		}},
   255  
   256  		{name: "large number to any", allocs: 1, f: func() {
   257  			anyHandle = Make[any](randomNumber)
   258  		}},
   259  
   260  		{name: "pair", allocs: 0, f: func() {
   261  			pairHandle = Make([2]string{heapString, heapString})
   262  		}},
   263  
   264  		{name: "pair from stack", allocs: 0, f: func() {
   265  			var b [16]byte
   266  			b[8] = 'a'
   267  			pairHandle = Make([2]string{string(b[:]), string(b[:])})
   268  		}},
   269  
   270  		{name: "pair to any", allocs: 1, f: func() {
   271  			anyHandle = Make[any]([2]string{heapString, heapString})
   272  		}},
   273  	}
   274  
   275  	for _, tt := range tests {
   276  		allocs := testing.AllocsPerRun(100, tt.f)
   277  		if allocs != float64(tt.allocs) {
   278  			errorf("%s: got %v allocs, want %v", tt.name, allocs, tt.allocs)
   279  		}
   280  	}
   281  }
   282  
   283  //go:noinline
   284  func newHeapBytes() []byte {
   285  	const N = 100
   286  	b := make([]byte, N)
   287  	for i := range b {
   288  		b[i] = byte(i)
   289  	}
   290  	return b
   291  }
   292  
   293  //go:noinline
   294  func newHeapString() string {
   295  	return string(newHeapBytes())
   296  }
   297  

View as plain text