File: hashmap_test.go

package info (click to toggle)
golang-github-xiaq-persistent 0.0~git20180301.cd415c6-1
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 172 kB
  • sloc: makefile: 27
file content (332 lines) | stat: -rw-r--r-- 8,331 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
package hashmap

import (
	"math/rand"
	"strconv"
	"testing"
	"time"

	"github.com/xiaq/persistent/hash"
)

const (
	NSequential = 0x1000
	NCollision  = 0x100
	NRandom     = 0x4000
	NReplace    = 0x200

	SmallRandomPass      = 0x100
	NSmallRandom         = 0x400
	SmallRandomHighBound = 0x50
	SmallRandomLowBound  = 0x200

	NArrayNode = 0x100

	NIneffectiveDissoc = 0x200

	N1 = nodeCap + 1
	N2 = nodeCap*nodeCap + 1
	N3 = nodeCap*nodeCap*nodeCap + 1
)

type testKey uint64
type anotherTestKey uint32

func equalFunc(k1, k2 interface{}) bool {
	switch k1 := k1.(type) {
	case testKey:
		t2, ok := k2.(testKey)
		return ok && k1 == t2
	case anotherTestKey:
		return false
	default:
		return k1 == k2
	}
}

func hashFunc(k interface{}) uint32 {
	switch k := k.(type) {
	case uint32:
		return k
	case string:
		return hash.String(k)
	case testKey:
		// Return the lower 32 bits for testKey. This is intended so that hash
		// collisions can be easily constructed.
		return uint32(k & 0xffffffff)
	case anotherTestKey:
		return uint32(k)
	default:
		return 0
	}
}

var empty = New(equalFunc, hashFunc)

type refEntry struct {
	k testKey
	v string
}

func hex(i uint64) string {
	return "0x" + strconv.FormatUint(i, 16)
}

func init() {
	rand.Seed(time.Now().UTC().UnixNano())
}

var randomStrings []string

// getRandomStrings returns a slice of N3 random strings. It builds the slice
// once and caches it. If the slice is built for the first time, it stops the
// timer of the benchmark.
func getRandomStrings(b *testing.B) []string {
	if randomStrings == nil {
		b.StopTimer()
		defer b.StartTimer()
		randomStrings = make([]string, N3)
		for i := 0; i < N3; i++ {
			randomStrings[i] = makeRandomString()
		}
	}
	return randomStrings
}

// makeRandomString builds a random string consisting of n bytes (randomized
// between 0 and 99) and each byte is randomized between 0 and 255. The string
// need not be valid UTF-8.
func makeRandomString() string {
	bytes := make([]byte, rand.Intn(100))
	for i := range bytes {
		bytes[i] = byte(rand.Intn(256))
	}
	return string(bytes)
}

func TestHashMap(t *testing.T) {
	var refEntries []refEntry
	add := func(k testKey, v string) {
		refEntries = append(refEntries, refEntry{k, v})
	}

	for i := 0; i < NSequential; i++ {
		add(testKey(i), hex(uint64(i)))
	}
	for i := 0; i < NCollision; i++ {
		add(testKey(uint64(i+1)<<32), "collision "+hex(uint64(i)))
	}
	for i := 0; i < NRandom; i++ {
		// Avoid rand.Uint64 for compatibility with pre 1.8 Go
		k := uint64(rand.Int63())>>31 | uint64(rand.Int63())<<32
		add(testKey(k), "random "+hex(k))
	}
	for i := 0; i < NReplace; i++ {
		k := uint64(rand.Int31n(NSequential))
		add(testKey(k), "replace "+hex(k))
	}

	testHashMapWithRefEntries(t, refEntries)
}

func TestHashMapSmallRandom(t *testing.T) {
	for p := 0; p < SmallRandomPass; p++ {
		var refEntries []refEntry
		add := func(k testKey, v string) {
			refEntries = append(refEntries, refEntry{k, v})
		}

		for i := 0; i < NSmallRandom; i++ {
			k := uint64(uint64(rand.Int31n(SmallRandomHighBound))<<32 |
				uint64(rand.Int31n(SmallRandomLowBound)))
			add(testKey(k), "random "+hex(k))
		}

		testHashMapWithRefEntries(t, refEntries)
	}
}

var marshalJSONTests = []struct {
	in      Map
	wantOut string
	wantErr bool
}{
	{makeHashMap(uint32(1), "a", "2", "b"), `{"1":"a","2":"b"}`, false},
	// Invalid key type
	{makeHashMap([]interface{}{}, "x"), "", true},
}

func TestMarshalJSON(t *testing.T) {
	for i, test := range marshalJSONTests {
		out, err := test.in.MarshalJSON()
		if string(out) != test.wantOut {
			t.Errorf("m%d.MarshalJSON -> out %s, want %s", i, out, test.wantOut)
		}
		if (err != nil) != test.wantErr {
			var wantErr string
			if test.wantErr {
				wantErr = "non-nil"
			} else {
				wantErr = "nil"
			}
			t.Errorf("m%d.MarshalJSON -> err %v, want %s", i, err, wantErr)
		}
	}
}

func makeHashMap(data ...interface{}) Map {
	m := empty
	for i := 0; i+1 < len(data); i += 2 {
		k, v := data[i], data[i+1]
		m = m.Assoc(k, v)
	}
	return m
}

// testHashMapWithRefEntries tests the operations of a Map. It uses the supplied
// list of entries to build the map, and then test all its operations.
func testHashMapWithRefEntries(t *testing.T, refEntries []refEntry) {
	m := empty
	// Len of Empty should be 0.
	if m.Len() != 0 {
		t.Errorf("m.Len = %d, want %d", m.Len(), 0)
	}

	// Assoc and Len, test by building a map simutaneously.
	ref := make(map[testKey]string, len(refEntries))
	for _, e := range refEntries {
		ref[e.k] = e.v
		m = m.Assoc(e.k, e.v)
		if m.Len() != len(ref) {
			t.Errorf("m.Len = %d, want %d", m.Len(), len(ref))
		}
	}

	// Index.
	testMapContent(t, m, ref)
	got, in := m.Index(anotherTestKey(0))
	if in {
		t.Errorf("m.Index <bad key> returns entry %v", got)
	}
	// Iterator.
	testIterator(t, m, ref)

	// Dissoc.
	// Ineffective ones.
	for i := 0; i < NIneffectiveDissoc; i++ {
		k := anotherTestKey(uint32(rand.Int31())>>15 | uint32(rand.Int31())<<16)
		m = m.Dissoc(k)
		if m.Len() != len(ref) {
			t.Errorf("m.Dissoc removes item when it shouldn't")
		}
	}

	// Effective ones.
	for i := len(refEntries) - 1; i >= 0; i-- {
		k := refEntries[i].k
		delete(ref, k)
		m = m.Dissoc(k)
		if m.Len() != len(ref) {
			t.Errorf("m.Len() = %d after removing, should be %v", m.Len(), len(ref))
		}
		_, in := m.Index(k)
		if in {
			t.Errorf("m.Index(%v) still returns item after removal", k)
		}
		// Checking all elements is expensive. Only do this 1% of the time.
		if rand.Float64() < 0.01 {
			testMapContent(t, m, ref)
		}
	}
}

func testMapContent(t *testing.T, m Map, ref map[testKey]string) {
	for k, v := range ref {
		got, in := m.Index(k)
		if !in {
			t.Errorf("m.Index 0x%x returns no entry", k)
		}
		if got != v {
			t.Errorf("m.Index(0x%x) = %v, want %v", k, got, v)
		}
	}
}

func testIterator(t *testing.T, m Map, ref map[testKey]string) {
	ref2 := map[interface{}]interface{}{}
	for k, v := range ref {
		ref2[k] = v
	}
	for it := m.Iterator(); it.HasElem(); it.Next() {
		k, v := it.Elem()
		if ref2[k] != v {
			t.Errorf("iterator yields unexpected pair %v, %v", k, v)
		}
		delete(ref2, k)
	}
	if len(ref2) != 0 {
		t.Errorf("iterating was not exhaustive")
	}
}

func BenchmarkSequentialConsNative1(b *testing.B) { nativeSequentialAdd(b.N, N1) }
func BenchmarkSequentialConsNative2(b *testing.B) { nativeSequentialAdd(b.N, N2) }
func BenchmarkSequentialConsNative3(b *testing.B) { nativeSequentialAdd(b.N, N3) }

// nativeSequntialAdd starts with an empty native map and adds elements 0...n-1
// to the map, using the same value as the key, repeating for N times.
func nativeSequentialAdd(N int, n uint32) {
	for r := 0; r < N; r++ {
		m := make(map[uint32]uint32)
		for i := uint32(0); i < n; i++ {
			m[i] = i
		}
	}
}

func BenchmarkSequentialConsPersistent1(b *testing.B) { sequentialCons(b.N, N1) }
func BenchmarkSequentialConsPersistent2(b *testing.B) { sequentialCons(b.N, N2) }
func BenchmarkSequentialConsPersistent3(b *testing.B) { sequentialCons(b.N, N3) }

// sequentialCons starts with an empty hash map and adds elements 0...n-1 to the
// map, using the same value as the key, repeating for N times.
func sequentialCons(N int, n uint32) {
	for r := 0; r < N; r++ {
		m := empty
		for i := uint32(0); i < n; i++ {
			m = m.Assoc(i, i)
		}
	}
}

func BenchmarkRandomStringsConsNative1(b *testing.B) { nativeRandomStringsAdd(b, N1) }
func BenchmarkRandomStringsConsNative2(b *testing.B) { nativeRandomStringsAdd(b, N2) }
func BenchmarkRandomStringsConsNative3(b *testing.B) { nativeRandomStringsAdd(b, N3) }

// nativeSequntialAdd starts with an empty native map and adds n random strings
// to the map, using the same value as the key, repeating for b.N times.
func nativeRandomStringsAdd(b *testing.B, n int) {
	ss := getRandomStrings(b)
	for r := 0; r < b.N; r++ {
		m := make(map[string]string)
		for i := 0; i < n; i++ {
			s := ss[i]
			m[s] = s
		}
	}
}

func BenchmarkRandomStringsConsPersistent1(b *testing.B) { randomStringsCons(b, N1) }
func BenchmarkRandomStringsConsPersistent2(b *testing.B) { randomStringsCons(b, N2) }
func BenchmarkRandomStringsConsPersistent3(b *testing.B) { randomStringsCons(b, N3) }

func randomStringsCons(b *testing.B, n int) {
	ss := getRandomStrings(b)
	for r := 0; r < b.N; r++ {
		m := empty
		for i := 0; i < n; i++ {
			s := ss[i]
			m = m.Assoc(s, s)
		}
	}
}