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

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}