github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/persistent/hashmap/hashmap_test.go (about)

     1  package hashmap
     2  
     3  import (
     4  	"math/rand"
     5  	"reflect"
     6  	"strconv"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/markusbkk/elvish/pkg/persistent/hash"
    11  )
    12  
    13  const (
    14  	NSequential = 0x1000
    15  	NCollision  = 0x100
    16  	NRandom     = 0x4000
    17  	NReplace    = 0x200
    18  
    19  	SmallRandomPass      = 0x100
    20  	NSmallRandom         = 0x400
    21  	SmallRandomHighBound = 0x50
    22  	SmallRandomLowBound  = 0x200
    23  
    24  	NArrayNode = 0x100
    25  
    26  	NIneffectiveDissoc = 0x200
    27  
    28  	N1 = nodeCap + 1
    29  	N2 = nodeCap*nodeCap + 1
    30  	N3 = nodeCap*nodeCap*nodeCap + 1
    31  )
    32  
    33  type testKey uint64
    34  type anotherTestKey uint32
    35  
    36  func equalFunc(k1, k2 interface{}) bool {
    37  	switch k1 := k1.(type) {
    38  	case testKey:
    39  		t2, ok := k2.(testKey)
    40  		return ok && k1 == t2
    41  	case anotherTestKey:
    42  		return false
    43  	default:
    44  		return k1 == k2
    45  	}
    46  }
    47  
    48  func hashFunc(k interface{}) uint32 {
    49  	switch k := k.(type) {
    50  	case uint32:
    51  		return k
    52  	case string:
    53  		return hash.String(k)
    54  	case testKey:
    55  		// Return the lower 32 bits for testKey. This is intended so that hash
    56  		// collisions can be easily constructed.
    57  		return uint32(k & 0xffffffff)
    58  	case anotherTestKey:
    59  		return uint32(k)
    60  	default:
    61  		return 0
    62  	}
    63  }
    64  
    65  var empty = New(equalFunc, hashFunc)
    66  
    67  type refEntry struct {
    68  	k testKey
    69  	v string
    70  }
    71  
    72  func hex(i uint64) string {
    73  	return "0x" + strconv.FormatUint(i, 16)
    74  }
    75  
    76  func init() {
    77  	rand.Seed(time.Now().UTC().UnixNano())
    78  }
    79  
    80  var randomStrings []string
    81  
    82  // getRandomStrings returns a slice of N3 random strings. It builds the slice
    83  // once and caches it. If the slice is built for the first time, it stops the
    84  // timer of the benchmark.
    85  func getRandomStrings(b *testing.B) []string {
    86  	if randomStrings == nil {
    87  		b.StopTimer()
    88  		defer b.StartTimer()
    89  		randomStrings = make([]string, N3)
    90  		for i := 0; i < N3; i++ {
    91  			randomStrings[i] = makeRandomString()
    92  		}
    93  	}
    94  	return randomStrings
    95  }
    96  
    97  // makeRandomString builds a random string consisting of n bytes (randomized
    98  // between 0 and 99) and each byte is randomized between 0 and 255. The string
    99  // need not be valid UTF-8.
   100  func makeRandomString() string {
   101  	bytes := make([]byte, rand.Intn(100))
   102  	for i := range bytes {
   103  		bytes[i] = byte(rand.Intn(256))
   104  	}
   105  	return string(bytes)
   106  }
   107  
   108  func TestHashMap(t *testing.T) {
   109  	var refEntries []refEntry
   110  	add := func(k testKey, v string) {
   111  		refEntries = append(refEntries, refEntry{k, v})
   112  	}
   113  
   114  	for i := 0; i < NSequential; i++ {
   115  		add(testKey(i), hex(uint64(i)))
   116  	}
   117  	for i := 0; i < NCollision; i++ {
   118  		add(testKey(uint64(i+1)<<32), "collision "+hex(uint64(i)))
   119  	}
   120  	for i := 0; i < NRandom; i++ {
   121  		// Avoid rand.Uint64 for compatibility with pre 1.8 Go
   122  		k := uint64(rand.Int63())>>31 | uint64(rand.Int63())<<32
   123  		add(testKey(k), "random "+hex(k))
   124  	}
   125  	for i := 0; i < NReplace; i++ {
   126  		k := uint64(rand.Int31n(NSequential))
   127  		add(testKey(k), "replace "+hex(k))
   128  	}
   129  
   130  	testHashMapWithRefEntries(t, refEntries)
   131  }
   132  
   133  func TestHashMapSmallRandom(t *testing.T) {
   134  	for p := 0; p < SmallRandomPass; p++ {
   135  		var refEntries []refEntry
   136  		add := func(k testKey, v string) {
   137  			refEntries = append(refEntries, refEntry{k, v})
   138  		}
   139  
   140  		for i := 0; i < NSmallRandom; i++ {
   141  			k := uint64(uint64(rand.Int31n(SmallRandomHighBound))<<32 |
   142  				uint64(rand.Int31n(SmallRandomLowBound)))
   143  			add(testKey(k), "random "+hex(k))
   144  		}
   145  
   146  		testHashMapWithRefEntries(t, refEntries)
   147  	}
   148  }
   149  
   150  var marshalJSONTests = []struct {
   151  	in      Map
   152  	wantOut string
   153  	wantErr bool
   154  }{
   155  	{makeHashMap(uint32(1), "a", "2", "b"), `{"1":"a","2":"b"}`, false},
   156  	// Invalid key type
   157  	{makeHashMap([]interface{}{}, "x"), "", true},
   158  }
   159  
   160  func TestMarshalJSON(t *testing.T) {
   161  	for i, test := range marshalJSONTests {
   162  		out, err := test.in.MarshalJSON()
   163  		if string(out) != test.wantOut {
   164  			t.Errorf("m%d.MarshalJSON -> out %s, want %s", i, out, test.wantOut)
   165  		}
   166  		if (err != nil) != test.wantErr {
   167  			var wantErr string
   168  			if test.wantErr {
   169  				wantErr = "non-nil"
   170  			} else {
   171  				wantErr = "nil"
   172  			}
   173  			t.Errorf("m%d.MarshalJSON -> err %v, want %s", i, err, wantErr)
   174  		}
   175  	}
   176  }
   177  
   178  func makeHashMap(data ...interface{}) Map {
   179  	m := empty
   180  	for i := 0; i+1 < len(data); i += 2 {
   181  		k, v := data[i], data[i+1]
   182  		m = m.Assoc(k, v)
   183  	}
   184  	return m
   185  }
   186  
   187  // testHashMapWithRefEntries tests the operations of a Map. It uses the supplied
   188  // list of entries to build the map, and then test all its operations.
   189  func testHashMapWithRefEntries(t *testing.T, refEntries []refEntry) {
   190  	m := empty
   191  	// Len of Empty should be 0.
   192  	if m.Len() != 0 {
   193  		t.Errorf("m.Len = %d, want %d", m.Len(), 0)
   194  	}
   195  
   196  	// Assoc and Len, test by building a map simultaneously.
   197  	ref := make(map[testKey]string, len(refEntries))
   198  	for _, e := range refEntries {
   199  		ref[e.k] = e.v
   200  		m = m.Assoc(e.k, e.v)
   201  		if m.Len() != len(ref) {
   202  			t.Errorf("m.Len = %d, want %d", m.Len(), len(ref))
   203  		}
   204  	}
   205  
   206  	// Index.
   207  	testMapContent(t, m, ref)
   208  	got, in := m.Index(anotherTestKey(0))
   209  	if in {
   210  		t.Errorf("m.Index <bad key> returns entry %v", got)
   211  	}
   212  	// Iterator.
   213  	testIterator(t, m, ref)
   214  
   215  	// Dissoc.
   216  	// Ineffective ones.
   217  	for i := 0; i < NIneffectiveDissoc; i++ {
   218  		k := anotherTestKey(uint32(rand.Int31())>>15 | uint32(rand.Int31())<<16)
   219  		m = m.Dissoc(k)
   220  		if m.Len() != len(ref) {
   221  			t.Errorf("m.Dissoc removes item when it shouldn't")
   222  		}
   223  	}
   224  
   225  	// Effective ones.
   226  	for x := 0; x < len(refEntries); x++ {
   227  		i := rand.Intn(len(refEntries))
   228  		k := refEntries[i].k
   229  		delete(ref, k)
   230  		m = m.Dissoc(k)
   231  		if m.Len() != len(ref) {
   232  			t.Errorf("m.Len() = %d after removing, should be %v", m.Len(), len(ref))
   233  		}
   234  		_, in := m.Index(k)
   235  		if in {
   236  			t.Errorf("m.Index(%v) still returns item after removal", k)
   237  		}
   238  		// Checking all elements is expensive. Only do this 1% of the time.
   239  		if rand.Float64() < 0.01 {
   240  			testMapContent(t, m, ref)
   241  			testIterator(t, m, ref)
   242  		}
   243  	}
   244  }
   245  
   246  func testMapContent(t *testing.T, m Map, ref map[testKey]string) {
   247  	for k, v := range ref {
   248  		got, in := m.Index(k)
   249  		if !in {
   250  			t.Errorf("m.Index 0x%x returns no entry", k)
   251  		}
   252  		if got != v {
   253  			t.Errorf("m.Index(0x%x) = %v, want %v", k, got, v)
   254  		}
   255  	}
   256  }
   257  
   258  func testIterator(t *testing.T, m Map, ref map[testKey]string) {
   259  	ref2 := map[interface{}]interface{}{}
   260  	for k, v := range ref {
   261  		ref2[k] = v
   262  	}
   263  	for it := m.Iterator(); it.HasElem(); it.Next() {
   264  		k, v := it.Elem()
   265  		if ref2[k] != v {
   266  			t.Errorf("iterator yields unexpected pair %v, %v", k, v)
   267  		}
   268  		delete(ref2, k)
   269  	}
   270  	if len(ref2) != 0 {
   271  		t.Errorf("iterating was not exhaustive")
   272  	}
   273  }
   274  
   275  func TestNilKey(t *testing.T) {
   276  	m := empty
   277  
   278  	testLen := func(l int) {
   279  		if m.Len() != l {
   280  			t.Errorf(".Len -> %d, want %d", m.Len(), l)
   281  		}
   282  	}
   283  	testIndex := func(wantVal interface{}, wantOk bool) {
   284  		val, ok := m.Index(nil)
   285  		if val != wantVal {
   286  			t.Errorf(".Index -> %v, want %v", val, wantVal)
   287  		}
   288  		if ok != wantOk {
   289  			t.Errorf(".Index -> ok %v, want %v", ok, wantOk)
   290  		}
   291  	}
   292  
   293  	testLen(0)
   294  	testIndex(nil, false)
   295  
   296  	m = m.Assoc(nil, "nil value")
   297  	testLen(1)
   298  	testIndex("nil value", true)
   299  
   300  	m = m.Assoc(nil, "nil value 2")
   301  	testLen(1)
   302  	testIndex("nil value 2", true)
   303  
   304  	m = m.Dissoc(nil)
   305  	testLen(0)
   306  	testIndex(nil, false)
   307  }
   308  
   309  func TestIterateMapWithNilKey(t *testing.T) {
   310  	m := empty.Assoc("k", "v").Assoc(nil, "nil value")
   311  	var collected []interface{}
   312  	for it := m.Iterator(); it.HasElem(); it.Next() {
   313  		k, v := it.Elem()
   314  		collected = append(collected, k, v)
   315  	}
   316  	wantCollected := []interface{}{nil, "nil value", "k", "v"}
   317  	if !reflect.DeepEqual(collected, wantCollected) {
   318  		t.Errorf("collected %v, want %v", collected, wantCollected)
   319  	}
   320  }
   321  
   322  func BenchmarkSequentialConjNative1(b *testing.B) { nativeSequentialAdd(b.N, N1) }
   323  func BenchmarkSequentialConjNative2(b *testing.B) { nativeSequentialAdd(b.N, N2) }
   324  func BenchmarkSequentialConjNative3(b *testing.B) { nativeSequentialAdd(b.N, N3) }
   325  
   326  // nativeSequntialAdd starts with an empty native map and adds elements 0...n-1
   327  // to the map, using the same value as the key, repeating for N times.
   328  func nativeSequentialAdd(N int, n uint32) {
   329  	for r := 0; r < N; r++ {
   330  		m := make(map[uint32]uint32)
   331  		for i := uint32(0); i < n; i++ {
   332  			m[i] = i
   333  		}
   334  	}
   335  }
   336  
   337  func BenchmarkSequentialConjPersistent1(b *testing.B) { sequentialConj(b.N, N1) }
   338  func BenchmarkSequentialConjPersistent2(b *testing.B) { sequentialConj(b.N, N2) }
   339  func BenchmarkSequentialConjPersistent3(b *testing.B) { sequentialConj(b.N, N3) }
   340  
   341  // sequentialConj starts with an empty hash map and adds elements 0...n-1 to the
   342  // map, using the same value as the key, repeating for N times.
   343  func sequentialConj(N int, n uint32) {
   344  	for r := 0; r < N; r++ {
   345  		m := empty
   346  		for i := uint32(0); i < n; i++ {
   347  			m = m.Assoc(i, i)
   348  		}
   349  	}
   350  }
   351  
   352  func BenchmarkRandomStringsConjNative1(b *testing.B) { nativeRandomStringsAdd(b, N1) }
   353  func BenchmarkRandomStringsConjNative2(b *testing.B) { nativeRandomStringsAdd(b, N2) }
   354  func BenchmarkRandomStringsConjNative3(b *testing.B) { nativeRandomStringsAdd(b, N3) }
   355  
   356  // nativeSequntialAdd starts with an empty native map and adds n random strings
   357  // to the map, using the same value as the key, repeating for b.N times.
   358  func nativeRandomStringsAdd(b *testing.B, n int) {
   359  	ss := getRandomStrings(b)
   360  	for r := 0; r < b.N; r++ {
   361  		m := make(map[string]string)
   362  		for i := 0; i < n; i++ {
   363  			s := ss[i]
   364  			m[s] = s
   365  		}
   366  	}
   367  }
   368  
   369  func BenchmarkRandomStringsConjPersistent1(b *testing.B) { randomStringsConj(b, N1) }
   370  func BenchmarkRandomStringsConjPersistent2(b *testing.B) { randomStringsConj(b, N2) }
   371  func BenchmarkRandomStringsConjPersistent3(b *testing.B) { randomStringsConj(b, N3) }
   372  
   373  func randomStringsConj(b *testing.B, n int) {
   374  	ss := getRandomStrings(b)
   375  	for r := 0; r < b.N; r++ {
   376  		m := empty
   377  		for i := 0; i < n; i++ {
   378  			s := ss[i]
   379  			m = m.Assoc(s, s)
   380  		}
   381  	}
   382  }