muldiv_test.gno
3.42 Kb · 114 lines
1package xmath
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7)
8
9func TestMulDiv(t *testing.T) {
10 cases := []struct {
11 name string
12 a, b, c int64
13 want int64
14 }{
15 {"exact", 10, 3, 5, 6},
16 {"rounds toward zero", 10, 1, 3, 3},
17 {"identity", 7, 1, 1, 7},
18 {"zero numerator", 0, 99, 7, 0},
19 {"half of a pot", 1000000, 50, 100, 500000},
20 {"one basis point", 1000000, 1, 10000, 100},
21 }
22 for _, tc := range cases {
23 t.Run(tc.name, func(t *testing.T) {
24 uassert.Equal(t, tc.want, MulDiv(tc.a, tc.b, tc.c))
25 })
26 }
27}
28
29// TestMulDivSurvivesAnIntermediateThatDoesNotFit is the whole point. The naive
30// a*b/c computes a*b first, which wraps past the int64 maximum and wraps to a
31// plausible number rather than an obvious one. Through a 128-bit intermediate
32// the answer is exact.
33func TestMulDivSurvivesAnIntermediateThatDoesNotFit(t *testing.T) {
34 // A variable, not a const: gno rejects a constant expression that
35 // overflows at compile time, and the point here is the runtime wrap.
36 big := int64(1) << 62 // 4611686018427387904
37 four := int64(4)
38
39 // big*4 overflows int64; big*4/8 is big/2 and fits comfortably.
40 uassert.Equal(t, big/2, MulDiv(big, four, 8))
41
42 // The naive form, computed here so the test shows what it would have done
43 // rather than asserting it in prose.
44 naive := (big * four) / 8
45 uassert.NotEqual(t, big/2, naive)
46
47 // big*4 is exactly 2^64, so it wraps to 0 and the naive form answers 0 for
48 // a share that is actually 2305843009213693952. Measured, not assumed: the
49 // first version of this test asserted the wrap went negative, which is the
50 // intuition and is wrong for this input.
51 uassert.Equal(t, int64(0), naive)
52}
53
54func TestMulDivRefusesRatherThanReturningAWrongNumber(t *testing.T) {
55 big := int64(1) << 62
56 cases := []struct {
57 name string
58 a, b, c int64
59 msg string
60 }{
61 {"negative a", -1, 2, 3, "xmath: MulDiv on a negative operand"},
62 {"negative b", 1, -2, 3, "xmath: MulDiv on a negative operand"},
63 {"zero denominator", 1, 2, 0, "xmath: MulDiv by a non-positive denominator"},
64 {"negative denominator", 1, 2, -3, "xmath: MulDiv by a non-positive denominator"},
65 {"quotient too large", big, 8, 2, "xmath: MulDiv quotient overflows int64"},
66 }
67 for _, tc := range cases {
68 t.Run(tc.name, func(t *testing.T) {
69 defer func() {
70 r := recover()
71 if r == nil {
72 t.Errorf("MulDiv(%d,%d,%d) did not panic", tc.a, tc.b, tc.c)
73 return
74 }
75 uassert.Equal(t, tc.msg, r.(string))
76 }()
77 MulDiv(tc.a, tc.b, tc.c)
78 })
79 }
80}
81
82func TestMulDivUpRoundsAwayFromZero(t *testing.T) {
83 cases := []struct {
84 name string
85 a, b, c int64
86 want int64
87 }{
88 {"exact is unchanged", 10, 3, 5, 6},
89 {"inexact rounds up", 10, 1, 3, 4},
90 {"one short rounds up", 7, 1, 2, 4},
91 {"zero stays zero", 0, 5, 3, 0},
92 }
93 for _, tc := range cases {
94 t.Run(tc.name, func(t *testing.T) {
95 uassert.Equal(t, tc.want, MulDivUp(tc.a, tc.b, tc.c))
96 })
97 }
98}
99
100// TestRoundingDirectionIsTheMoneyQuestion states the invariant the two
101// functions exist to give a caller: what is received never exceeds what is
102// owed for the same ratio, so a pool cannot pay out more than it holds.
103func TestRoundingDirectionIsTheMoneyQuestion(t *testing.T) {
104 for _, n := range []int64{1, 2, 3, 7, 99, 1000001} {
105 down := MulDiv(n, 1, 3)
106 up := MulDivUp(n, 1, 3)
107 if up < down {
108 t.Errorf("MulDivUp(%d,1,3)=%d below MulDiv=%d", n, up, down)
109 }
110 if up-down > 1 {
111 t.Errorf("MulDivUp(%d,1,3)=%d more than one above MulDiv=%d", n, up, down)
112 }
113 }
114}