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

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}