Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}