From 3d2a39a18c3e6d11373f4b6739323d9db75ccc6b Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 10 Dec 2018 11:23:19 +0100 Subject: [PATCH] autopilot/choice_test: add chooseN tests --- autopilot/choice_test.go | 196 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 autopilot/choice_test.go diff --git a/autopilot/choice_test.go b/autopilot/choice_test.go new file mode 100644 index 000000000..cabfa11db --- /dev/null +++ b/autopilot/choice_test.go @@ -0,0 +1,196 @@ +package autopilot + +import ( + "encoding/binary" + "math/rand" + "reflect" + "testing" + "testing/quick" +) + +var ( + nID1 = NodeID([33]byte{1}) + nID2 = NodeID([33]byte{2}) + nID3 = NodeID([33]byte{3}) + nID4 = NodeID([33]byte{4}) +) + +// TestChooseNEmptyMap checks that chooseN returns an empty result when no +// nodes are chosen among. +func TestChooseNEmptyMap(t *testing.T) { + t.Parallel() + + nodes := map[NodeID]*AttachmentDirective{} + 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]*AttachmentDirective + +// 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]*AttachmentDirective) + + // 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] = &AttachmentDirective{ + 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]*AttachmentDirective) 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]*AttachmentDirective) + + // 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] = &AttachmentDirective{ + 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]) + } + } + } +}