prorata_test.gno
4.68 Kb · 147 lines
1package prorata
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7)
8
9func sum(xs []int64) int64 {
10 t := int64(0)
11 for _, x := range xs {
12 t += x
13 }
14 return t
15}
16
17func TestSplitTable(t *testing.T) {
18 cases := []struct {
19 name string
20 amount int64
21 weights []int64
22 want []int64
23 }{
24 {"exact thirds", 99, []int64{1, 1, 1}, []int64{33, 33, 33}},
25 {"one unit of dust, to the lowest index", 10, []int64{1, 1, 1}, []int64{4, 3, 3}},
26 {"two units of dust", 11, []int64{1, 1, 1}, []int64{4, 4, 3}},
27 {"uneven weights", 100, []int64{1, 2, 3}, []int64{17, 33, 50}},
28 {"a single claimant takes it all", 7, []int64{5}, []int64{7}},
29 {"zero weight is paid nothing", 10, []int64{0, 1, 1}, []int64{0, 5, 5}},
30 {"zero weight is not paid dust either", 10, []int64{0, 1, 1, 1}, []int64{0, 4, 3, 3}},
31 {"nothing to split", 0, []int64{3, 1}, []int64{0, 0}},
32 {"amount smaller than the roster", 2, []int64{1, 1, 1, 1}, []int64{1, 1, 0, 0}},
33 {"weight dominates", 1000, []int64{999, 1}, []int64{999, 1}},
34 }
35 for _, tc := range cases {
36 got, err := Split(tc.amount, tc.weights)
37 uassert.NoError(t, err, tc.name)
38 uassert.Equal(t, len(tc.want), len(got), tc.name)
39 for i := range tc.want {
40 uassert.Equal(t, tc.want[i], got[i], tc.name)
41 }
42 }
43}
44
45// TestSplitAlwaysSumsToTheAmount is the whole point: a split that is short by
46// dust strands coins in the realm forever, and one that is over cannot be paid.
47func TestSplitAlwaysSumsToTheAmount(t *testing.T) {
48 weightsets := [][]int64{
49 {1, 1, 1},
50 {1, 2, 3, 4, 5, 6, 7},
51 {7, 0, 1},
52 {100, 1},
53 {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
54 {1},
55 {999983, 17, 5},
56 }
57 for _, ws := range weightsets {
58 for amount := int64(0); amount < 60; amount++ {
59 got, err := Split(amount, ws)
60 uassert.NoError(t, err)
61 uassert.Equal(t, amount, sum(got), "shares must sum to the amount")
62 }
63 for _, amount := range []int64{1000, 123457, 999999999} {
64 got, err := Split(amount, ws)
65 uassert.NoError(t, err)
66 uassert.Equal(t, amount, sum(got))
67 }
68 }
69}
70
71// TestLargestRemainderWins pins the allocation rule, so a payout cannot quietly
72// change to "whoever the loop paid last".
73func TestLargestRemainderWins(t *testing.T) {
74 // total 10, amount 4: floors are 0,0,1,1 with remainders 4,8,2,6.
75 got, err := Split(4, []int64{1, 2, 3, 4})
76 uassert.NoError(t, err)
77 uassert.Equal(t, int64(0), got[0], "smallest remainder, no extra unit")
78 uassert.Equal(t, int64(1), got[1], "largest remainder, gets one")
79 uassert.Equal(t, int64(1), got[2], "floor 1, remainder 2, gets none")
80 uassert.Equal(t, int64(2), got[3], "second largest, gets the other")
81 uassert.Equal(t, int64(4), sum(got))
82}
83
84func TestSplitValidation(t *testing.T) {
85 _, err := Split(-1, []int64{1})
86 uassert.ErrorIs(t, err, ErrNegativeAmount)
87
88 _, err = Split(10, []int64{1, -1})
89 uassert.ErrorIs(t, err, ErrNegativeWeight)
90
91 _, err = Split(10, nil)
92 uassert.ErrorIs(t, err, ErrNoWeight, "an empty roster is the caller's problem, not dust")
93
94 _, err = Split(10, []int64{0, 0})
95 uassert.ErrorIs(t, err, ErrNoWeight, "every weight zero is the same problem")
96}
97
98// TestNoOverflowOnLargePots is the second failure mode: amount*weight wraps
99// negative long before either factor is anywhere near the limit.
100func TestNoOverflowOnLargePots(t *testing.T) {
101 // A pot of 10^18 ugnot split across weights that would overflow if
102 // multiplied together first.
103 pot := int64(1000000000000000000)
104 got, err := Split(pot, []int64{3000000000, 1000000000})
105 uassert.NoError(t, err)
106 uassert.Equal(t, pot, sum(got))
107 uassert.Equal(t, int64(750000000000000000), got[0])
108 uassert.Equal(t, int64(250000000000000000), got[1])
109
110 got, err = Split(maxInt64, []int64{1, 1})
111 uassert.NoError(t, err)
112 uassert.Equal(t, maxInt64, sum(got), "even the largest possible pot")
113
114 _, err = Total([]int64{maxInt64, 1})
115 uassert.ErrorIs(t, err, ErrOverflow, "a wrapped total makes every share nonsense")
116}
117
118func TestShare(t *testing.T) {
119 got, err := Share(100, 1, 3)
120 uassert.NoError(t, err)
121 uassert.Equal(t, int64(33), got, "rounded down, the remainder is Split's job")
122
123 got, err = Share(1000000000000000000, 3000000000, 4000000000)
124 uassert.NoError(t, err)
125 uassert.Equal(t, int64(750000000000000000), got)
126
127 got, err = Share(100, 7, 3)
128 uassert.NoError(t, err)
129 uassert.Equal(t, int64(100), got, "a weight above the total is clamped, never over-paid")
130
131 _, err = Share(10, 1, 0)
132 uassert.ErrorIs(t, err, ErrNoWeight)
133 _, err = Share(-1, 1, 2)
134 uassert.ErrorIs(t, err, ErrNegativeAmount)
135 _, err = Share(10, -1, 2)
136 uassert.ErrorIs(t, err, ErrNegativeWeight)
137}
138
139func TestTotal(t *testing.T) {
140 got, err := Total([]int64{1, 2, 3})
141 uassert.NoError(t, err)
142 uassert.Equal(t, int64(6), got)
143
144 got, err = Total(nil)
145 uassert.NoError(t, err)
146 uassert.Equal(t, int64(0), got, "empty sums to zero; Split is what refuses it")
147}