github.com/decred/dcrlnd@v0.7.6/autopilot/choice_test.go (about) 1 package autopilot 2 3 import ( 4 "encoding/binary" 5 "math/rand" 6 "reflect" 7 "testing" 8 "testing/quick" 9 ) 10 11 // TestWeightedChoiceEmptyMap tests that passing in an empty slice of weights 12 // returns an error. 13 func TestWeightedChoiceEmptyMap(t *testing.T) { 14 t.Parallel() 15 16 var w []float64 17 _, err := weightedChoice(w) 18 if err != ErrNoPositive { 19 t.Fatalf("expected ErrNoPositive when choosing in "+ 20 "empty map, instead got %v", err) 21 } 22 } 23 24 // singeNonZero is a type used to generate float64 slices with one non-zero 25 // element. 26 type singleNonZero []float64 27 28 // Generate generates a value of type sinelNonZero to be used during 29 // QuickTests. 30 func (singleNonZero) Generate(rand *rand.Rand, size int) reflect.Value { 31 w := make([]float64, size) 32 33 // Pick a random index and set it to a random float. 34 i := rand.Intn(size) 35 w[i] = rand.Float64() 36 37 return reflect.ValueOf(w) 38 } 39 40 // TestWeightedChoiceSingleIndex tests that choosing randomly in a slice with 41 // one positive element always returns that one index. 42 func TestWeightedChoiceSingleIndex(t *testing.T) { 43 t.Parallel() 44 45 // Helper that returns the index of the non-zero element. 46 allButOneZero := func(weights []float64) (bool, int) { 47 var ( 48 numZero uint32 49 nonZeroEl int 50 ) 51 52 for i, w := range weights { 53 if w != 0 { 54 numZero++ 55 nonZeroEl = i 56 } 57 } 58 59 return numZero == 1, nonZeroEl 60 } 61 62 property := func(weights singleNonZero) bool { 63 // Make sure the generated slice has exactly one non-zero 64 // element. 65 conditionMet, nonZeroElem := allButOneZero(weights[:]) 66 if !conditionMet { 67 return false 68 } 69 70 // Call weightedChoice and assert it picks the non-zero 71 // element. 72 choice, err := weightedChoice(weights[:]) 73 if err != nil { 74 return false 75 } 76 return choice == nonZeroElem 77 } 78 79 if err := quick.Check(property, nil); err != nil { 80 t.Fatal(err) 81 } 82 } 83 84 // nonNegative is a type used to generate float64 slices with non-negative 85 // elements. 86 type nonNegative []float64 87 88 // Generate generates a value of type nonNegative to be used during 89 // QuickTests. 90 func (nonNegative) Generate(rand *rand.Rand, size int) reflect.Value { 91 w := make([]float64, size) 92 93 for i := range w { 94 r := rand.Float64() 95 96 // For very small weights it won't work to check deviation from 97 // expected value, so we set them to zero. 98 if r < 0.01*float64(size) { 99 r = 0 100 } 101 w[i] = r 102 } 103 return reflect.ValueOf(w) 104 } 105 106 func assertChoice(w []float64, iterations int) bool { 107 var sum float64 108 for _, v := range w { 109 sum += v 110 } 111 112 // Calculate the expected frequency of each choice. 113 expFrequency := make([]float64, len(w)) 114 for i, ww := range w { 115 expFrequency[i] = ww / sum 116 } 117 118 chosen := make(map[int]int) 119 for i := 0; i < iterations; i++ { 120 res, err := weightedChoice(w) 121 if err != nil { 122 return false 123 } 124 chosen[res]++ 125 } 126 127 // Since this is random we check that the number of times chosen is 128 // within 20% of the expected value. 129 totalChoices := 0 130 for i, f := range expFrequency { 131 exp := float64(iterations) * f 132 v := float64(chosen[i]) 133 totalChoices += chosen[i] 134 expHigh := exp + exp/5 135 expLow := exp - exp/5 136 if v < expLow || v > expHigh { 137 return false 138 } 139 } 140 141 // The sum of choices must be exactly iterations of course. 142 return totalChoices == iterations 143 } 144 145 // TestWeightedChoiceDistribution asserts that the weighted choice algorithm 146 // chooses among indexes according to their scores. 147 func TestWeightedChoiceDistribution(t *testing.T) { 148 const iterations = 100000 149 150 property := func(weights nonNegative) bool { 151 return assertChoice(weights, iterations) 152 } 153 154 if err := quick.Check(property, nil); err != nil { 155 t.Fatal(err) 156 } 157 } 158 159 // TestChooseNEmptyMap checks that chooseN returns an empty result when no 160 // nodes are chosen among. 161 func TestChooseNEmptyMap(t *testing.T) { 162 t.Parallel() 163 164 nodes := map[NodeID]*NodeScore{} 165 property := func(n uint32) bool { 166 res, err := chooseN(n, nodes) 167 if err != nil { 168 return false 169 } 170 171 // Result should always be empty. 172 return len(res) == 0 173 } 174 175 if err := quick.Check(property, nil); err != nil { 176 t.Fatal(err) 177 } 178 } 179 180 // candidateMapVarLen is a type we'll use to generate maps of various lengths 181 // up to 255 to be used during QuickTests. 182 type candidateMapVarLen map[NodeID]*NodeScore 183 184 // Generate generates a value of type candidateMapVarLen to be used during 185 // QuickTests. 186 func (candidateMapVarLen) Generate(rand *rand.Rand, size int) reflect.Value { 187 nodes := make(map[NodeID]*NodeScore) 188 189 // To avoid creating huge maps, we restrict them to max uint8 len. 190 n := uint8(rand.Uint32()) 191 192 for i := uint8(0); i < n; i++ { 193 s := rand.Float64() 194 195 // We set small values to zero, to ensure we handle these 196 // correctly. 197 if s < 0.01 { 198 s = 0 199 } 200 201 var nID [33]byte 202 binary.BigEndian.PutUint32(nID[:], uint32(i)) 203 nodes[nID] = &NodeScore{ 204 Score: s, 205 } 206 } 207 208 return reflect.ValueOf(nodes) 209 } 210 211 // TestChooseNMinimum test that chooseN returns the minimum of the number of 212 // nodes we request and the number of positively scored nodes in the given map. 213 func TestChooseNMinimum(t *testing.T) { 214 t.Parallel() 215 216 // Helper to count the number of positive scores in the given map. 217 numPositive := func(nodes map[NodeID]*NodeScore) int { 218 cnt := 0 219 for _, v := range nodes { 220 if v.Score > 0 { 221 cnt++ 222 } 223 } 224 return cnt 225 } 226 227 // We use let the type of n be uint8 to avoid generating huge numbers. 228 property := func(nodes candidateMapVarLen, n uint8) bool { 229 res, err := chooseN(uint32(n), nodes) 230 if err != nil { 231 return false 232 } 233 234 positive := numPositive(nodes) 235 236 // Result should always be the minimum of the number of nodes 237 // we wanted to select and the number of positively scored 238 // nodes in the map. 239 min := positive 240 if int(n) < min { 241 min = int(n) 242 } 243 244 if len(res) != min { 245 return false 246 247 } 248 return true 249 } 250 251 if err := quick.Check(property, nil); err != nil { 252 t.Fatal(err) 253 } 254 } 255 256 // TestChooseNSample sanity checks that nodes are picked by chooseN according 257 // to their scores. 258 func TestChooseNSample(t *testing.T) { 259 t.Parallel() 260 261 const numNodes = 500 262 const maxIterations = 100000 263 fifth := uint32(numNodes / 5) 264 265 nodes := make(map[NodeID]*NodeScore) 266 267 // we make 5 buckets of nodes: 0, 0.1, 0.2, 0.4 and 0.8 score. We want 268 // to check that zero scores never gets chosen, while a doubling the 269 // score makes a node getting chosen about double the amount (this is 270 // true only when n <<< numNodes). 271 j := 2 * fifth 272 score := 0.1 273 for i := uint32(0); i < numNodes; i++ { 274 275 // Each time i surpasses j we double the score we give to the 276 // next fifth of nodes. 277 if i >= j { 278 score *= 2 279 j += fifth 280 } 281 s := score 282 283 // The first 1/5 of nodes we give a score of 0. 284 if i < fifth { 285 s = 0 286 } 287 288 var nID [33]byte 289 binary.BigEndian.PutUint32(nID[:], i) 290 nodes[nID] = &NodeScore{ 291 Score: s, 292 } 293 } 294 295 // For each value of N we'll check that the nodes are picked the 296 // expected number of times over time. 297 for _, n := range []uint32{1, 5, 10, 20, 50} { 298 // Since choosing more nodes will result in chooseN getting 299 // slower we decrease the number of iterations. This is okay 300 // since the variance in the total picks for a node will be 301 // lower when choosing more nodes each time. 302 iterations := maxIterations / n 303 count := make(map[NodeID]int) 304 for i := 0; i < int(iterations); i++ { 305 res, err := chooseN(n, nodes) 306 if err != nil { 307 t.Fatalf("failed choosing nodes: %v", err) 308 } 309 310 for nID := range res { 311 count[nID]++ 312 } 313 } 314 315 // Sum the number of times a node in each score bucket was 316 // picked. 317 sums := make(map[float64]int) 318 for nID, s := range nodes { 319 sums[s.Score] += count[nID] 320 } 321 322 // The count of each bucket should be about double of the 323 // previous bucket. Since this is all random, we check that 324 // the result is within 20% of the expected value. 325 for _, score := range []float64{0.2, 0.4, 0.8} { 326 cnt := sums[score] 327 half := cnt / 2 328 expLow := half - half/5 329 expHigh := half + half/5 330 if sums[score/2] < expLow || sums[score/2] > expHigh { 331 t.Fatalf("expected the nodes with score %v "+ 332 "to be chosen about %v times, instead "+ 333 "was %v", score/2, half, sums[score/2]) 334 } 335 } 336 } 337 }