forked from lightningnetwork/lnd
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathchoice_test.go
338 lines (281 loc) · 7.97 KB
/
choice_test.go
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
333
334
335
336
337
338
package autopilot
import (
"encoding/binary"
"math/rand"
"reflect"
"testing"
"testing/quick"
)
// TestWeightedChoiceEmptyMap tests that passing in an empty slice of weights
// returns an error.
func TestWeightedChoiceEmptyMap(t *testing.T) {
t.Parallel()
var w []float64
_, err := weightedChoice(w)
if err != ErrNoPositive {
t.Fatalf("expected ErrNoPositive when choosing in "+
"empty map, instead got %v", err)
}
}
// singeNonZero is a type used to generate float64 slices with one non-zero
// element.
type singleNonZero []float64
// Generate generates a value of type sinelNonZero to be used during
// QuickTests.
func (singleNonZero) Generate(rand *rand.Rand, size int) reflect.Value {
w := make([]float64, size)
// Pick a random index and set it to a random float.
i := rand.Intn(size)
w[i] = rand.Float64()
return reflect.ValueOf(w)
}
// TestWeightedChoiceSingleIndex tests that choosing randomly in a slice with
// one positive element always returns that one index.
func TestWeightedChoiceSingleIndex(t *testing.T) {
t.Parallel()
// Helper that returns the index of the non-zero element.
allButOneZero := func(weights []float64) (bool, int) {
var (
numZero uint32
nonZeroEl int
)
for i, w := range weights {
if w != 0 {
numZero++
nonZeroEl = i
}
}
return numZero == 1, nonZeroEl
}
property := func(weights singleNonZero) bool {
// Make sure the generated slice has exactly one non-zero
// element.
conditionMet, nonZeroElem := allButOneZero(weights[:])
if !conditionMet {
return false
}
// Call weightedChoice and assert it picks the non-zero
// element.
choice, err := weightedChoice(weights[:])
if err != nil {
return false
}
return choice == nonZeroElem
}
if err := quick.Check(property, nil); err != nil {
t.Fatal(err)
}
}
// nonNegative is a type used to generate float64 slices with non-negative
// elements.
type nonNegative []float64
// Generate generates a value of type nonNegative to be used during
// QuickTests.
func (nonNegative) Generate(rand *rand.Rand, size int) reflect.Value {
w := make([]float64, size)
for i := range w {
r := rand.Float64()
// For very small weights it won't work to check deviation from
// expected value, so we set them to zero.
if r < 0.01*float64(size) {
r = 0
}
w[i] = float64(r)
}
return reflect.ValueOf(w)
}
func assertChoice(w []float64, iterations int) bool {
var sum float64
for _, v := range w {
sum += v
}
// Calculate the expected frequency of each choice.
expFrequency := make([]float64, len(w))
for i, ww := range w {
expFrequency[i] = ww / sum
}
chosen := make(map[int]int)
for i := 0; i < iterations; i++ {
res, err := weightedChoice(w)
if err != nil {
return false
}
chosen[res]++
}
// Since this is random we check that the number of times chosen is
// within 20% of the expected value.
totalChoices := 0
for i, f := range expFrequency {
exp := float64(iterations) * f
v := float64(chosen[i])
totalChoices += chosen[i]
expHigh := exp + exp/5
expLow := exp - exp/5
if v < expLow || v > expHigh {
return false
}
}
// The sum of choices must be exactly iterations of course.
return totalChoices == iterations
}
// TestWeightedChoiceDistribution asserts that the weighted choice algorithm
// chooses among indexes according to their scores.
func TestWeightedChoiceDistribution(t *testing.T) {
const iterations = 100000
property := func(weights nonNegative) bool {
return assertChoice(weights, iterations)
}
if err := quick.Check(property, nil); err != nil {
t.Fatal(err)
}
}
// TestChooseNEmptyMap checks that chooseN returns an empty result when no
// nodes are chosen among.
func TestChooseNEmptyMap(t *testing.T) {
t.Parallel()
nodes := map[NodeID]*NodeScore{}
property := func(n uint32) bool {
res, err := chooseN(n, nodes)
if err != nil {
return false
}
// Result should always be empty.
return len(res) == 0
}
if err := quick.Check(property, nil); err != nil {
t.Fatal(err)
}
}
// candidateMapVarLen is a type we'll use to generate maps of various lengths
// up to 255 to be used during QuickTests.
type candidateMapVarLen map[NodeID]*NodeScore
// Generate generates a value of type candidateMapVarLen to be used during
// QuickTests.
func (candidateMapVarLen) Generate(rand *rand.Rand, size int) reflect.Value {
nodes := make(map[NodeID]*NodeScore)
// To avoid creating huge maps, we restrict them to max uint8 len.
n := uint8(rand.Uint32())
for i := uint8(0); i < n; i++ {
s := rand.Float64()
// We set small values to zero, to ensure we handle these
// correctly.
if s < 0.01 {
s = 0
}
var nID [33]byte
binary.BigEndian.PutUint32(nID[:], uint32(i))
nodes[nID] = &NodeScore{
Score: s,
}
}
return reflect.ValueOf(nodes)
}
// TestChooseNMinimum test that chooseN returns the minimum of the number of
// nodes we request and the number of positively scored nodes in the given map.
func TestChooseNMinimum(t *testing.T) {
t.Parallel()
// Helper to count the number of positive scores in the given map.
numPositive := func(nodes map[NodeID]*NodeScore) int {
cnt := 0
for _, v := range nodes {
if v.Score > 0 {
cnt++
}
}
return cnt
}
// We use let the type of n be uint8 to avoid generating huge numbers.
property := func(nodes candidateMapVarLen, n uint8) bool {
res, err := chooseN(uint32(n), nodes)
if err != nil {
return false
}
positive := numPositive(nodes)
// Result should always be the minimum of the number of nodes
// we wanted to select and the number of positively scored
// nodes in the map.
min := positive
if int(n) < min {
min = int(n)
}
if len(res) != min {
return false
}
return true
}
if err := quick.Check(property, nil); err != nil {
t.Fatal(err)
}
}
// TestChooseNSample sanity checks that nodes are picked by chooseN according
// to their scores.
func TestChooseNSample(t *testing.T) {
t.Parallel()
const numNodes = 500
const maxIterations = 100000
fifth := uint32(numNodes / 5)
nodes := make(map[NodeID]*NodeScore)
// we make 5 buckets of nodes: 0, 0.1, 0.2, 0.4 and 0.8 score. We want
// to check that zero scores never gets chosen, while a doubling the
// score makes a node getting chosen about double the amount (this is
// true only when n <<< numNodes).
j := 2 * fifth
score := 0.1
for i := uint32(0); i < numNodes; i++ {
// Each time i surpasses j we double the score we give to the
// next fifth of nodes.
if i >= j {
score *= 2
j += fifth
}
s := score
// The first 1/5 of nodes we give a score of 0.
if i < fifth {
s = 0
}
var nID [33]byte
binary.BigEndian.PutUint32(nID[:], i)
nodes[nID] = &NodeScore{
Score: s,
}
}
// For each value of N we'll check that the nodes are picked the
// expected number of times over time.
for _, n := range []uint32{1, 5, 10, 20, 50} {
// Since choosing more nodes will result in chooseN getting
// slower we decrease the number of iterations. This is okay
// since the variance in the total picks for a node will be
// lower when choosing more nodes each time.
iterations := maxIterations / n
count := make(map[NodeID]int)
for i := 0; i < int(iterations); i++ {
res, err := chooseN(n, nodes)
if err != nil {
t.Fatalf("failed choosing nodes: %v", err)
}
for nID := range res {
count[nID]++
}
}
// Sum the number of times a node in each score bucket was
// picked.
sums := make(map[float64]int)
for nID, s := range nodes {
sums[s.Score] += count[nID]
}
// The count of each bucket should be about double of the
// previous bucket. Since this is all random, we check that
// the result is within 20% of the expected value.
for _, score := range []float64{0.2, 0.4, 0.8} {
cnt := sums[score]
half := cnt / 2
expLow := half - half/5
expHigh := half + half/5
if sums[score/2] < expLow || sums[score/2] > expHigh {
t.Fatalf("expected the nodes with score %v "+
"to be chosen about %v times, instead "+
"was %v", score/2, half, sums[score/2])
}
}
}
}