compact_test.gno
7.70 Kb · 235 lines
1package compact
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/moul/kit/ui/v0"
8 "gno.land/p/moul/x/storagecost/v1"
9 "gno.land/p/nt/testutils/v0"
10 "gno.land/p/nt/uassert/v0"
11)
12
13var (
14 alice = testutils.TestAddress("alice")
15 bob = testutils.TestAddress("bob")
16 // janitor never adds anything, it only compacts. The whole point of the
17 // realm is that this is the address the chain pays.
18 janitor = testutils.TestAddress("janitor")
19)
20
21// drain empties the board and asserts it, so a test starts from a known
22// state whatever ran before it. Indices are append-addressed for the life of
23// the realm, so every test asserts on counts and never on absolute indices.
24func drain(cur realm, t *testing.T) {
25 t.Helper()
26 for i := 0; i < entries.TotalSize(); i++ {
27 if e, ok := entryAt(i); ok {
28 testing.SetRealm(testing.NewUserRealm(e.Author))
29 Drop(cross(cur), i)
30 }
31 }
32 testing.SetRealm(testing.NewUserRealm(janitor))
33 Compact(cross(cur))
34 live, _ := Fragmentation()
35 uassert.Equal(t, 0, live)
36}
37
38func TestDropFreesNothingUntilCompact(cur realm, t *testing.T) {
39 drain(cur, t)
40 testing.SetRealm(testing.NewUserRealm(alice))
41 for i := 0; i < 8; i++ {
42 Add(cross(cur), "entry")
43 }
44 live, allocated := Fragmentation()
45 uassert.Equal(t, 8, live)
46 uassert.Equal(t, 0, Reclaimable())
47
48 // Dropping everything moves the live count but frees no nodes by itself.
49 base := allocated - 8
50 for i := base; i < allocated; i++ {
51 Drop(cross(cur), i)
52 }
53 live, allocated2 := Fragmentation()
54 uassert.Equal(t, 0, live)
55 uassert.Equal(t, allocated, allocated2, "a soft delete must not change the index space")
56 uassert.True(t, Reclaimable() > 0, "a fully dead tail should be reclaimable")
57
58 // And compaction is what actually frees them.
59 testing.SetRealm(testing.NewUserRealm(janitor))
60 freed := Compact(cross(cur))
61 uassert.True(t, freed > 0)
62 uassert.Equal(t, 0, Reclaimable())
63 // Compacting twice is not an error, it just frees nothing, so two bots
64 // racing for the same nodes both survive rather than one aborting.
65 uassert.Equal(t, 0, Compact(cross(cur)))
66}
67
68// TestScatteredHolesReclaimLessThanTheGap is the claim the Render makes, and
69// the reason the realm publishes two numbers instead of one. No other test
70// could catch it: they all drop contiguous runs.
71func TestScatteredHolesReclaimLessThanTheGap(cur realm, t *testing.T) {
72 drain(cur, t)
73 testing.SetRealm(testing.NewUserRealm(alice))
74 base := entries.TotalSize()
75 for i := 0; i < 16; i++ {
76 Add(cross(cur), "entry")
77 }
78 // Every other index, so no whole subtree dies.
79 for i := base; i < base+16; i += 2 {
80 Drop(cross(cur), i)
81 }
82 live, allocated := Fragmentation()
83 gap := allocated - live
84 uassert.True(t, gap >= 8, "eight holes were made")
85 uassert.True(t, Reclaimable() < gap,
86 "scattered holes must reclaim fewer nodes than the gap suggests")
87
88 drain(cur, t)
89}
90
91func TestOnlyTheAuthorMayDrop(cur realm, t *testing.T) {
92 drain(cur, t)
93 testing.SetRealm(testing.NewUserRealm(alice))
94 i := Add(cross(cur), "alice's entry")
95
96 testing.SetRealm(testing.NewUserRealm(bob))
97 uassert.AbortsWithMessage(t, cur, "compact: only the author may drop an entry", func() {
98 Drop(cross(cur), i)
99 })
100 // But compaction stays permissionless, which is the asymmetry that makes
101 // the mechanism work: choosing what dies is owned, reclaiming is not.
102 testing.SetRealm(testing.NewUserRealm(janitor))
103 uassert.NotAborts(t, cur, func() { Compact(cross(cur)) })
104
105 drain(cur, t)
106}
107
108func TestAddRejectsBadInput(cur realm, t *testing.T) {
109 drain(cur, t)
110 testing.SetRealm(testing.NewUserRealm(alice))
111 uassert.AbortsWithMessage(t, cur, "compact: empty entry", func() {
112 Add(cross(cur), "")
113 })
114 uassert.AbortsWithMessage(t, cur, "compact: entry too long, 257 bytes against a 256 cap", func() {
115 Add(cross(cur), strings.Repeat("x", maxText+1))
116 })
117 uassert.AbortsContains(t, cur, "compact: no live entry at", func() {
118 Drop(cross(cur), 99999)
119 })
120}
121
122// TestQuoteCountsNodesRatherThanGuessingFromPayload is the design claim of the
123// realm. The quote must depend on the NODE COUNT and not on how fat the
124// entries were, which is exactly where a payload-derived bounty goes wrong.
125func TestQuoteCountsNodesRatherThanGuessingFromPayload(cur realm, t *testing.T) {
126 drain(cur, t)
127
128 // An empty board advertises nothing and must not claim a profit.
129 empty := Quote()
130 uassert.Equal(t, int64(0), empty.Bytes)
131 uassert.False(t, empty.Worth())
132
133 // Eight tiny entries, dropped and measured.
134 testing.SetRealm(testing.NewUserRealm(alice))
135 base := entries.TotalSize()
136 for i := 0; i < 8; i++ {
137 Add(cross(cur), "x")
138 }
139 for i := base; i < base+8; i++ {
140 Drop(cross(cur), i)
141 }
142 tiny := Quote()
143 tinyNodes := Reclaimable()
144 testing.SetRealm(testing.NewUserRealm(janitor))
145 Compact(cross(cur))
146
147 // Eight fat entries, same count, same treatment.
148 testing.SetRealm(testing.NewUserRealm(alice))
149 base = entries.TotalSize()
150 fat := strings.Repeat("y", maxText)
151 for i := 0; i < 8; i++ {
152 Add(cross(cur), fat)
153 }
154 for i := base; i < base+8; i++ {
155 Drop(cross(cur), i)
156 }
157 big := Quote()
158
159 bigNodes := Reclaimable()
160
161 // 256x the payload, and the quote is still purely a function of the node
162 // count. Not the same TOTAL: the two batches sit at different offsets in
163 // an append-addressed index space, so the tree shape differs and one
164 // reclaimed a node more than the other. That is the honest version of the
165 // claim, and asserting equal totals (which an earlier draft of this test
166 // did) only passed by luck of the offsets.
167 uassert.True(t, tinyNodes > 0 && bigNodes > 0)
168 uassert.Equal(t, storagecost.EstimateNodes(int64(tinyNodes)), tiny.Bytes)
169 uassert.Equal(t, storagecost.EstimateNodes(int64(bigNodes)), big.Bytes)
170
171 // The rate is what must not move: same bytes per reclaimed node whether
172 // the entries were 1 byte or 256. A payload-derived bounty would differ
173 // by 256x here, which is the failure this realm is built to avoid.
174 uassert.Equal(t, tiny.Bytes/int64(tinyNodes), big.Bytes/int64(bigNodes),
175 "bytes per reclaimed node must not depend on payload size")
176
177 drain(cur, t)
178}
179
180// TestRenderEscapesTheBoard: an entry is a caller's string landing in an
181// inline markdown slot, so it goes through ui.Excerpt or it is an injection.
182func TestRenderEscapesTheBoard(cur realm, t *testing.T) {
183 drain(cur, t)
184 testing.SetRealm(testing.NewUserRealm(alice))
185 Add(cross(cur), "[Claim 100 GNOT](https://evil.example)\n# Official")
186
187 out := Render("")
188 uassert.False(t, strings.Contains(out, "](https://evil.example)"), out)
189 uassert.False(t, strings.Contains(out, "\n# Official"), out)
190 uassert.True(t, strings.Contains(out, "Claim 100 GNOT"), out)
191 uassert.True(t, strings.Contains(out, ui.Addr(alice)), out)
192
193 drain(cur, t)
194}
195
196func TestRenderShowsBothIntegersAndTheAction(cur realm, t *testing.T) {
197 drain(cur, t)
198
199 out := Render("")
200 uassert.True(t, strings.Contains(out, "# Compact"), out)
201 uassert.True(t, strings.Contains(out, "Add the first entry"), out)
202 uassert.True(t, strings.Contains(out, "nothing to reclaim"), out)
203
204 testing.SetRealm(testing.NewUserRealm(alice))
205 base := entries.TotalSize()
206 for i := 0; i < 8; i++ {
207 Add(cross(cur), "entry")
208 }
209 for i := base; i < base+8; i++ {
210 Drop(cross(cur), i)
211 }
212 out = Render("")
213 uassert.True(t, strings.Contains(out, "reclaimable nodes"), out)
214 uassert.True(t, strings.Contains(out, "$help&func=Compact"), out)
215
216 drain(cur, t)
217}
218
219// TestRenderNeverEmitsTwoBlankLines is what lets ExampleRender stay
220// meaningful: gno collapses them, so output containing them can never be
221// pinned by an example.
222func TestRenderNeverEmitsTwoBlankLines(cur realm, t *testing.T) {
223 drain(cur, t)
224 testing.SetRealm(testing.NewUserRealm(alice))
225 i := Add(cross(cur), "one")
226 Add(cross(cur), "two")
227 Drop(cross(cur), i)
228
229 for _, path := range []string{"", "anything"} {
230 out := Render(path)
231 uassert.False(t, strings.Contains(out, "\n\n\n"), "blank-line run in Render("+path+")")
232 }
233
234 drain(cur, t)
235}