clock_test.gno
7.08 Kb · 196 lines
1package clock
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7)
8
9func mustNew(t *testing.T, now, window, floor, life int64) *Clock {
10 t.Helper()
11 c, err := New(now, window, floor, life)
12 uassert.NoError(t, err)
13 return c
14}
15
16func TestNewValidation(t *testing.T) {
17 cases := []struct {
18 name string
19 now, window, floor, life int64
20 want error
21 }{
22 {"zero window", 0, 0, 0, 0, ErrBadWindow},
23 {"negative window", 0, -1, 0, 0, ErrBadWindow},
24 {"negative floor", 0, 100, -1, 0, ErrBadFloor},
25 {"floor above window", 0, 100, 101, 0, ErrBadFloor},
26 {"negative life", 0, 100, 10, -1, ErrBadLife},
27 {"life shorter than window", 0, 100, 10, 99, ErrBadLife},
28 {"window overflows", maxInt64 - 5, 100, 0, 0, ErrOverflow},
29 {"life overflows", maxInt64 - 500, 100, 0, 1000, ErrOverflow},
30 {"unbounded is legal", 0, 100, 10, 0, nil},
31 {"life equal to window is legal", 0, 100, 10, 100, nil},
32 {"floor equal to window is legal", 0, 100, 100, 0, nil},
33 }
34 for _, tc := range cases {
35 _, err := New(tc.now, tc.window, tc.floor, tc.life)
36 if tc.want == nil {
37 uassert.NoError(t, err, tc.name)
38 continue
39 }
40 uassert.ErrorIs(t, err, tc.want, tc.name)
41 }
42}
43
44func TestOpensDueAtWindow(t *testing.T) {
45 c := mustNew(t, 1000, 100, 10, 500)
46 uassert.Equal(t, int64(1000), c.Start())
47 uassert.Equal(t, int64(1100), c.Deadline())
48 uassert.Equal(t, int64(1500), c.HardEnd())
49 uassert.Equal(t, int64(100), c.Remaining(1000))
50 uassert.Equal(t, int64(0), c.Elapsed(1000))
51 uassert.False(t, c.Expired(1099))
52 uassert.True(t, c.Expired(1100), "the deadline itself is expired")
53 uassert.False(t, c.Final())
54}
55
56func TestUnboundedHasNoHardEnd(t *testing.T) {
57 c := mustNew(t, 0, 100, 0, 0)
58 uassert.Equal(t, int64(0), c.HardEnd())
59 uassert.False(t, c.Final(), "an unbounded clock is never final")
60 c.Bump(90)
61 c.Bump(180)
62 uassert.Equal(t, int64(280), c.Deadline(), "nothing caps it")
63}
64
65// TestBumpNeverMovesBackwards is the invariant a caller supplying its own grant
66// gets wrong first: a late, small grant must not shorten the clock.
67func TestBumpNeverMovesBackwards(t *testing.T) {
68 c := mustNew(t, 0, 1000, 0, 0)
69 uassert.Equal(t, int64(1000), c.Deadline())
70
71 uassert.Equal(t, int64(1000), c.BumpBy(10, 5), "now+5 is before the deadline")
72 uassert.Equal(t, int64(1000), c.BumpBy(900, 0), "a zero grant leaves it alone")
73 uassert.Equal(t, int64(1100), c.BumpBy(900, 200), "a grant past the deadline moves it")
74}
75
76// TestFloorSurvivesATinyGrant is the anti-snipe: whatever the policy grants, a
77// bump leaves at least the floor, so the last stretch stays reachable.
78func TestFloorSurvivesATinyGrant(t *testing.T) {
79 c := mustNew(t, 0, 1000, 50, 0)
80 c.BumpBy(990, 1)
81 uassert.Equal(t, int64(1040), c.Deadline(), "1 was raised to the floor of 50")
82 uassert.Equal(t, int64(50), c.Remaining(990))
83
84 c2 := mustNew(t, 0, 1000, 50, 0)
85 c2.BumpBy(990, 0)
86 uassert.Equal(t, int64(50), c2.Remaining(990), "even a zero grant leaves the floor")
87}
88
89// TestHardEndCapsEveryBump is the terminator: without it a contested clock is
90// extended forever and the game has no end.
91func TestHardEndCapsEveryBump(t *testing.T) {
92 c := mustNew(t, 0, 100, 10, 250)
93 uassert.Equal(t, int64(250), c.HardEnd())
94
95 c.BumpBy(90, 100)
96 uassert.Equal(t, int64(190), c.Deadline())
97 c.BumpBy(180, 100)
98 uassert.Equal(t, int64(250), c.Deadline(), "capped at the hard end, not 280")
99 uassert.True(t, c.Final())
100
101 c.BumpBy(240, 1000000)
102 uassert.Equal(t, int64(250), c.Deadline(), "the cap holds against any grant")
103 uassert.Equal(t, int64(10), c.Remaining(240), "and it beats the floor")
104}
105
106// TestExpiredClockNeverRestarts stops a late action from reopening a settled
107// game, which is the difference between a closed pot and a stolen one.
108func TestExpiredClockNeverRestarts(t *testing.T) {
109 c := mustNew(t, 0, 100, 10, 0)
110 uassert.True(t, c.Expired(100))
111 uassert.Equal(t, int64(100), c.BumpBy(100, 500), "exactly at the deadline")
112 uassert.Equal(t, int64(100), c.Bump(5000), "long after")
113 uassert.Equal(t, int64(0), c.Remaining(100))
114
115 settled := mustNew(t, 0, 100, 10, 1000)
116 uassert.True(t, settled.Expired(100))
117 got, err := settled.BumpShare(200, 50)
118 uassert.NoError(t, err)
119 uassert.Equal(t, int64(100), got, "not even a large share reopens it")
120}
121
122func TestBumpShareValidation(t *testing.T) {
123 c := mustNew(t, 0, 100, 0, 500)
124 _, err := c.BumpShare(10, -1)
125 uassert.ErrorIs(t, err, ErrBadShare)
126 _, err = c.BumpShare(10, 101)
127 uassert.ErrorIs(t, err, ErrBadShare)
128 uassert.Equal(t, int64(100), c.Deadline(), "a rejected share changes nothing")
129
130 unbounded := mustNew(t, 0, 100, 0, 0)
131 _, err = unbounded.BumpShare(10, 50)
132 uassert.ErrorIs(t, err, ErrUnbounded, "no hard end, nothing to take a share of")
133}
134
135// TestBumpShareDecays is why the share grant exists: each action closes a
136// fraction of the gap to the hard end, so the grants shrink as the game runs
137// and the pressure to act one more time is relieved instead of compounding.
138func TestBumpShareDecays(t *testing.T) {
139 c := mustNew(t, 0, 100, 1, 1000)
140 first, err := c.BumpShare(10, 50)
141 uassert.NoError(t, err)
142 uassert.Equal(t, int64(505), first, "half the 990 left to the hard end")
143
144 second, err := c.BumpShare(500, 50)
145 uassert.NoError(t, err)
146 uassert.Equal(t, int64(750), second, "half of the 500 now left, a smaller grant")
147
148 third, err := c.BumpShare(700, 50)
149 uassert.NoError(t, err)
150 uassert.Equal(t, int64(850), third, "smaller again")
151 uassert.True(t, third < c.HardEnd(), "it approaches the hard end without reaching it")
152}
153
154// TestBumpShareRoundingStillGrants is the multiply-first lesson: a share of a
155// gap smaller than the divisor must not truncate to a zero grant.
156func TestBumpShareRoundingStillGrants(t *testing.T) {
157 // 52 due, 50 left to the hard end: a tenth of that is 5, and dividing
158 // before multiplying would truncate it to a grant of zero.
159 c := mustNew(t, 0, 52, 0, 100)
160 got, err := c.BumpShare(50, 10)
161 uassert.NoError(t, err)
162 uassert.Equal(t, int64(55), got, "a tenth of the 50 left is 5, not 0")
163}
164
165func TestBumpShareRespectsFloorAndCap(t *testing.T) {
166 c := mustNew(t, 0, 100, 20, 150)
167 got, err := c.BumpShare(95, 10) // a tenth of the 55 left is 5, floored to 20
168 uassert.NoError(t, err)
169 uassert.Equal(t, int64(115), got)
170
171 got, err = c.BumpShare(110, 100) // would be 150 exactly, and never past it
172 uassert.NoError(t, err)
173 uassert.Equal(t, int64(150), got)
174 uassert.True(t, c.Final())
175}
176
177// TestHugeGrantDoesNotWrap keeps a bump total at the extremes: a wrapped
178// deadline would land in the past and settle the game instantly.
179func TestHugeGrantDoesNotWrap(t *testing.T) {
180 c := mustNew(t, 0, 100, 0, 0)
181 uassert.Equal(t, maxInt64, c.BumpBy(50, maxInt64), "clamped, not wrapped")
182 uassert.True(t, c.Deadline() > 0)
183
184 big := mustNew(t, maxInt64/2, 1000, 0, maxInt64/4)
185 got, err := big.BumpShare(maxInt64/2+1, 100)
186 uassert.NoError(t, err)
187 uassert.True(t, got > maxInt64/2, "a huge remaining share does not wrap")
188}
189
190func TestElapsedAndRemainingAreNeverNegative(t *testing.T) {
191 c := mustNew(t, 100, 100, 0, 0)
192 uassert.Equal(t, int64(0), c.Elapsed(50), "before the start")
193 uassert.Equal(t, int64(100), c.Remaining(100))
194 uassert.Equal(t, int64(0), c.Remaining(9999), "long past the deadline")
195 uassert.Equal(t, int64(9899), c.Elapsed(9999))
196}