reaper_test.gno
7.84 Kb · 238 lines
1package reaper
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/moul/x/storagecost/v0"
8 "gno.land/p/nt/testutils/v0"
9 "gno.land/p/nt/uassert/v0"
10)
11
12var (
13 poster = testutils.TestAddress("poster")
14 // stranger never posts, it only deletes. The whole point of the realm is
15 // that this is the address the chain pays.
16 stranger = testutils.TestAddress("stranger")
17)
18
19// ttlFuture is the ttl used for a note that must not be reapable yet. It is a
20// named constant because clear has to outrun it.
21const ttlFuture = 1000
22
23// clear empties the board so a test starts from a known state, whatever ran
24// before it.
25//
26// gno resets the block height for each test function but keeps realm state, so
27// a test that left an unexpired note behind cannot be cleaned up by a later
28// test skipping heights: the later test's clock starts over. Every test that
29// posts a future-dated note therefore drains it before returning, and clear
30// asserts an empty board rather than trusting that.
31//
32// clear cannot reset TotalSize, which is append-addressed for the life of the
33// realm, so tests assert on Live and Reapable and never on absolute indices.
34func clear(cur realm, t *testing.T) {
35 t.Helper()
36 drain(cur)
37 uassert.Equal(t, 0, Live())
38 uassert.Equal(t, 0, Reapable())
39}
40
41// drain expires everything outstanding and reaps it.
42func drain(cur realm) {
43 testing.SkipHeights(ttlFuture + 1)
44 testing.SetRealm(testing.NewUserRealm(stranger))
45 for Reapable() > 0 {
46 Reap(cross(cur), 1000)
47 }
48 Compact(cross(cur))
49}
50
51func TestPostLocksAndReapFrees(cur realm, t *testing.T) {
52 clear(cur, t)
53
54 testing.SetRealm(testing.NewUserRealm(poster))
55 Post(cross(cur), "first", 0)
56 Post(cross(cur), "second", 0)
57 uassert.Equal(t, 2, Live())
58 uassert.Equal(t, 2, Reapable())
59
60 // A stranger who posted nothing may reap, and that is the point.
61 testing.SetRealm(testing.NewUserRealm(stranger))
62 uassert.Equal(t, 2, Reap(cross(cur), 100))
63 uassert.Equal(t, 0, Live())
64 uassert.Equal(t, 0, Reapable())
65}
66
67func TestReapSkipsUnexpiredAndHonoursLimit(cur realm, t *testing.T) {
68 clear(cur, t)
69 testing.SetRealm(testing.NewUserRealm(poster))
70
71 Post(cross(cur), "ripe one", 0)
72 Post(cross(cur), "ripe two", 0)
73 Post(cross(cur), "not yet", ttlFuture)
74 uassert.Equal(t, 3, Live())
75 uassert.Equal(t, 2, Reapable())
76
77 // The limit caps the work, so a reaper can size a transaction to its gas.
78 testing.SetRealm(testing.NewUserRealm(stranger))
79 uassert.Equal(t, 1, Reap(cross(cur), 1))
80 uassert.Equal(t, 1, Reapable())
81
82 // The unexpired note survives a reap that asks for everything.
83 uassert.Equal(t, 1, Reap(cross(cur), 100))
84 uassert.Equal(t, 0, Reapable())
85 uassert.Equal(t, 1, Live())
86
87 // Leave nothing behind: the next test's height starts over and could not
88 // expire this note.
89 drain(cur)
90}
91
92func TestReapOfNothingIsNotAnError(cur realm, t *testing.T) {
93 clear(cur, t)
94 // A bot that races another bot to the same notes must not revert, or it
95 // loses its gas to an abort instead of merely earning nothing.
96 testing.SetRealm(testing.NewUserRealm(stranger))
97 uassert.Equal(t, 0, Reap(cross(cur), 100))
98}
99
100func TestCompactFollowsReaping(cur realm, t *testing.T) {
101 clear(cur, t)
102 testing.SetRealm(testing.NewUserRealm(poster))
103 for i := 0; i < 8; i++ {
104 Post(cross(cur), "note", 0)
105 }
106
107 // Nothing is reclaimable until something has been reaped.
108 uassert.Equal(t, 0, Compactable())
109
110 testing.SetRealm(testing.NewUserRealm(stranger))
111 uassert.Equal(t, 8, Reap(cross(cur), 100))
112 uassert.True(t, Compactable() > 0)
113
114 freed := Compact(cross(cur))
115 uassert.True(t, freed > 0)
116 uassert.Equal(t, 0, Compactable())
117 // Compacting twice is not an error, it just frees nothing.
118 uassert.Equal(t, 0, Compact(cross(cur)))
119}
120
121func TestPostRejectsBadInput(cur realm, t *testing.T) {
122 clear(cur, t)
123 testing.SetRealm(testing.NewUserRealm(poster))
124
125 uassert.AbortsWithMessage(t, cur, "reaper: empty note", func() {
126 Post(cross(cur), "", 0)
127 })
128 uassert.AbortsWithMessage(t, cur, "reaper: negative ttl", func() {
129 Post(cross(cur), "fine", -1)
130 })
131 // The cap keeps one call from locking an unbounded deposit.
132 uassert.AbortsWithMessage(t, cur, "reaper: note too long, 4097 bytes against a 4096 cap", func() {
133 Post(cross(cur), strings.Repeat("x", maxBody+1), 0)
134 })
135 uassert.AbortsWithMessage(t, cur, "reaper: limit must be positive", func() {
136 Reap(cross(cur), 0)
137 })
138}
139
140func TestBountyPricesWhatIsOnTheTable(cur realm, t *testing.T) {
141 clear(cur, t)
142
143 // An empty board advertises nothing, and must not claim a profit.
144 empty := Bounty()
145 uassert.Equal(t, int64(0), empty.Bytes)
146 uassert.Equal(t, int64(0), empty.Refund)
147 uassert.False(t, empty.Worth())
148
149 testing.SetRealm(testing.NewUserRealm(poster))
150 body := strings.Repeat("x", 1024)
151 for i := 0; i < 10; i++ {
152 Post(cross(cur), body, 0)
153 }
154
155 // Ten 1 KB notes, priced through the same estimate the library documents.
156 q := Bounty()
157 uassert.Equal(t, storagecost.EstimateBytes(10*1024), q.Bytes)
158 uassert.Equal(t, storagecost.Refund(q.Bytes, storagecost.DefaultStoragePrice), q.Refund)
159 uassert.True(t, q.Worth())
160
161 // Once reaped, nothing is on the table.
162 testing.SetRealm(testing.NewUserRealm(stranger))
163 Reap(cross(cur), 100)
164 uassert.Equal(t, int64(0), Bounty().Bytes)
165}
166
167func TestRenderShowsTheBountyAndTheReapLink(cur realm, t *testing.T) {
168 clear(cur, t)
169
170 // Empty board: an invitation, and no bounty claimed.
171 out := Render("")
172 uassert.True(t, strings.Contains(out, "# Reaper"), out)
173 uassert.True(t, strings.Contains(out, "Nothing has expired"), out)
174 uassert.True(t, strings.Contains(out, "Post the first note"), out)
175
176 testing.SetRealm(testing.NewUserRealm(poster))
177 Post(cross(cur), strings.Repeat("y", 1024), 0)
178
179 out = Render("")
180 uassert.True(t, strings.Contains(out, "1 expired notes"), out)
181 uassert.True(t, strings.Contains(out, "refunds roughly **0.1894 GNOT**"), out)
182 // The reap link must be a help link for the right function with the right
183 // argument, or the button on the page does nothing useful. txlink builds
184 // it relative to the current realm, so there is no path to get wrong.
185 uassert.True(t, strings.Contains(out, "$help&func=Reap&limit=100"), out)
186 // The author is named, which is what makes "the poster paid" visible.
187 uassert.True(t, strings.Contains(out, poster.String()), out)
188 uassert.True(t, strings.Contains(out, "**reapable**"), out)
189
190 // A long note is summarized rather than dumped into the table.
191 uassert.True(t, strings.Contains(out, "..."), out)
192 uassert.False(t, strings.Contains(out, strings.Repeat("y", 200)), "body should be truncated")
193}
194
195func TestRenderNeverEmitsTwoBlankLines(cur realm, t *testing.T) {
196 // gno collapses two consecutive blank lines, so output containing them can
197 // never be pinned by an Example. This test is what lets ExampleRender stay
198 // meaningful.
199 clear(cur, t)
200 testing.SetRealm(testing.NewUserRealm(poster))
201 Post(cross(cur), "one", 0)
202 Post(cross(cur), "two", ttlFuture)
203 testing.SetRealm(testing.NewUserRealm(stranger))
204 Reap(cross(cur), 1)
205
206 for _, path := range []string{"", "anything"} {
207 out := Render(path)
208 uassert.False(t, strings.Contains(out, "\n\n\n"), "blank-line run in Render("+path+")")
209 }
210
211 drain(cur)
212}
213
214func TestReapWalksNewestFirstSoCompactionHasWork(cur realm, t *testing.T) {
215 // The ordering is economic, not cosmetic: in the backing list the oldest
216 // indices are the ancestors of the newest, so a partial reap that took the
217 // oldest first would leave nothing compactable and strand the tree
218 // structure, which costs more than the notes do.
219 clear(cur, t)
220 testing.SetRealm(testing.NewUserRealm(poster))
221 for i := 0; i < 16; i++ {
222 Post(cross(cur), "note", 0)
223 }
224 first := notes.TotalSize() - 16
225 last := notes.TotalSize() - 1
226
227 testing.SetRealm(testing.NewUserRealm(stranger))
228 uassert.Equal(t, 4, Reap(cross(cur), 4))
229
230 // The four highest indices went, and the four lowest survived.
231 uassert.Equal(t, nil, notes.Get(last))
232 uassert.True(t, notes.Get(first) != nil)
233
234 // Which is the whole point: there is something to compact after one batch.
235 uassert.True(t, Compactable() > 0)
236
237 drain(cur)
238}