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

index_test.gno

10.40 Kb · 367 lines
  1package index
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/moul/kit/store/v0"
  7	"gno.land/p/nt/uassert/v0"
  8	"gno.land/p/nt/urequire/v0"
  9)
 10
 11func ids(ns ...uint64) []store.ID {
 12	out := make([]store.ID, len(ns))
 13	for i, n := range ns {
 14		out[i] = store.ID(n)
 15	}
 16	return out
 17}
 18
 19func equalIDs(t *testing.T, want, got []store.ID) {
 20	t.Helper()
 21	urequire.Equal(t, len(want), len(got))
 22	for i := range want {
 23		uassert.Equal(t, want[i].String(), got[i].String())
 24	}
 25}
 26
 27func TestZeroValueIsUsable(t *testing.T) {
 28	var ix Index
 29	uassert.Equal(t, 0, ix.Keys())
 30	uassert.Equal(t, 0, ix.Len())
 31	uassert.False(t, ix.Has("gno"))
 32	uassert.Equal(t, 0, ix.Count("gno"))
 33	uassert.Equal(t, 0, len(ix.Lookup("gno")))
 34	_, ok := ix.First("gno")
 35	uassert.False(t, ok)
 36	uassert.False(t, ix.Remove("gno", 1))
 37	uassert.Equal(t, 0, ix.RemoveKey("gno"))
 38	uassert.False(t, ix.Each(func(string, []store.ID) bool { return true }))
 39
 40	urequire.NoError(t, ix.Add("gno", 1))
 41	uassert.Equal(t, 1, ix.Keys())
 42}
 43
 44// The whole point of sorting the ids: Lookup must depend on the SET of ids, not
 45// on the order they arrived in, or two realms that indexed the same records in
 46// a different order render differently.
 47func TestLookupDoesNotDependOnInsertionOrder(t *testing.T) {
 48	forward, reverse := New(), New()
 49	for _, n := range []uint64{3, 1, 2} {
 50		urequire.NoError(t, forward.Add("gno", store.ID(n)))
 51	}
 52	for _, n := range []uint64{2, 1, 3} {
 53		urequire.NoError(t, reverse.Add("gno", store.ID(n)))
 54	}
 55	equalIDs(t, ids(1, 2, 3), forward.Lookup("gno"))
 56	equalIDs(t, ids(1, 2, 3), reverse.Lookup("gno"))
 57}
 58
 59func TestAddIsIdempotentForTheSamePair(t *testing.T) {
 60	ix := New()
 61	urequire.NoError(t, ix.Add("gno", 7))
 62	urequire.NoError(t, ix.Add("gno", 7))
 63	equalIDs(t, ids(7), ix.Lookup("gno"))
 64	uassert.Equal(t, 1, ix.Len())
 65	uassert.Equal(t, 1, ix.Keys())
 66}
 67
 68func TestAddRefusals(t *testing.T) {
 69	for _, tc := range []struct {
 70		name string
 71		key  string
 72		id   store.ID
 73		want error
 74	}{
 75		{"empty key", "", 1, ErrEmptyKey},
 76		{"zero id", "gno", 0, ErrZeroID},
 77	} {
 78		t.Run(tc.name, func(t *testing.T) {
 79			ix := New()
 80			err := ix.Add(tc.key, tc.id)
 81			urequire.ErrorIs(t, err, tc.want)
 82			uassert.Equal(t, 0, ix.Len())
 83		})
 84	}
 85}
 86
 87func TestUniqueRefusesASecondID(t *testing.T) {
 88	ix := Unique()
 89	uassert.True(t, ix.IsUnique())
 90	urequire.NoError(t, ix.Add("moul", 1))
 91
 92	err := ix.Add("moul", 2)
 93	urequire.ErrorIs(t, err, ErrDuplicateKey)
 94	equalIDs(t, ids(1), ix.Lookup("moul"))
 95	uassert.Equal(t, 1, ix.Len())
 96
 97	// Re-adding the SAME pair is still a no-op, not a duplicate: a realm
 98	// re-running a write must not be punished for it.
 99	urequire.NoError(t, ix.Add("moul", 1))
100	uassert.Equal(t, 1, ix.Len())
101}
102
103// Keys and Len answer different questions and the difference is the fan-out,
104// which is what the index actually costs.
105func TestKeysAndLenDiffer(t *testing.T) {
106	ix := New()
107	urequire.NoError(t, ix.Add("gno", 1))
108	urequire.NoError(t, ix.Add("gno", 2))
109	urequire.NoError(t, ix.Add("go", 3))
110	uassert.Equal(t, 2, ix.Keys())
111	uassert.Equal(t, 3, ix.Len())
112	uassert.Equal(t, 2, ix.Count("gno"))
113	uassert.Equal(t, 1, ix.Count("go"))
114	uassert.Equal(t, 0, ix.Count("rust"))
115}
116
117func TestRemoveDropsTheKeyWithItsLastID(t *testing.T) {
118	ix := New()
119	urequire.NoError(t, ix.Add("gno", 1))
120	urequire.NoError(t, ix.Add("gno", 2))
121
122	uassert.True(t, ix.Remove("gno", 1))
123	uassert.True(t, ix.Has("gno"))
124	equalIDs(t, ids(2), ix.Lookup("gno"))
125
126	uassert.True(t, ix.Remove("gno", 2))
127	uassert.False(t, ix.Has("gno"))
128	uassert.Equal(t, 0, ix.Keys())
129	uassert.Equal(t, 0, ix.Len())
130
131	uassert.False(t, ix.Remove("gno", 2))
132}
133
134func TestRemoveKey(t *testing.T) {
135	ix := New()
136	urequire.NoError(t, ix.Add("gno", 1))
137	urequire.NoError(t, ix.Add("gno", 2))
138	urequire.NoError(t, ix.Add("go", 3))
139
140	uassert.Equal(t, 2, ix.RemoveKey("gno"))
141	uassert.Equal(t, 1, ix.Keys())
142	uassert.Equal(t, 1, ix.Len())
143	uassert.Equal(t, 0, ix.RemoveKey("gno"))
144}
145
146// A returned slice that is the stored one makes every caller a writer: an
147// assignment into it corrupts the index with no write path having been called.
148func TestLookupHandsOutACopy(t *testing.T) {
149	ix := New()
150	urequire.NoError(t, ix.Add("gno", 1))
151	urequire.NoError(t, ix.Add("gno", 2))
152
153	got := ix.Lookup("gno")
154	got[0] = store.ID(999)
155	equalIDs(t, ids(1, 2), ix.Lookup("gno"))
156}
157
158func TestEachIsAscendingAndStops(t *testing.T) {
159	ix := New()
160	for _, k := range []string{"go", "gno", "rust"} {
161		urequire.NoError(t, ix.Add(k, 1))
162	}
163
164	var seen []string
165	uassert.False(t, ix.Each(func(k string, _ []store.ID) bool {
166		seen = append(seen, k)
167		return false
168	}))
169	urequire.Equal(t, 3, len(seen))
170	uassert.Equal(t, "gno", seen[0])
171	uassert.Equal(t, "go", seen[1])
172	uassert.Equal(t, "rust", seen[2])
173
174	var first string
175	uassert.True(t, ix.Each(func(k string, _ []store.ID) bool {
176		first = k
177		return true
178	}))
179	uassert.Equal(t, "gno", first)
180}
181
182func TestEachHandsOutACopy(t *testing.T) {
183	ix := New()
184	urequire.NoError(t, ix.Add("gno", 1))
185	ix.Each(func(_ string, got []store.ID) bool {
186		got[0] = store.ID(999)
187		return true
188	})
189	equalIDs(t, ids(1), ix.Lookup("gno"))
190}
191
192func TestPage(t *testing.T) {
193	ix := New()
194	for _, k := range []string{"a", "b", "c", "d", "e"} {
195		urequire.NoError(t, ix.Add(k, 1))
196	}
197	for _, tc := range []struct {
198		name string
199		page int
200		size int
201		want []string
202	}{
203		{"first", 1, 2, []string{"a", "b"}},
204		{"second", 2, 2, []string{"c", "d"}},
205		{"last, short", 3, 2, []string{"e"}},
206		{"past the end", 4, 2, nil},
207		{"page zero", 0, 2, nil},
208		{"size zero", 1, 0, nil},
209		{"negative page", -1, 2, nil},
210	} {
211		t.Run(tc.name, func(t *testing.T) {
212			got := ix.Page(tc.page, tc.size)
213			urequire.Equal(t, len(tc.want), len(got))
214			for i := range tc.want {
215				uassert.Equal(t, tc.want[i], got[i].Key)
216			}
217		})
218	}
219}
220
221// Pages is what a page picker divides by, so it is never 0.
222func TestPagesIsNeverZero(t *testing.T) {
223	ix := New()
224	uassert.Equal(t, 1, ix.Pages(10))
225	uassert.Equal(t, 1, ix.Pages(0))
226	uassert.Equal(t, 1, ix.Pages(-1))
227
228	for _, k := range []string{"a", "b", "c"} {
229		urequire.NoError(t, ix.Add(k, 1))
230	}
231	uassert.Equal(t, 2, ix.Pages(2))
232	uassert.Equal(t, 1, ix.Pages(3))
233	uassert.Equal(t, 3, ix.Pages(1))
234}
235
236func TestFirst(t *testing.T) {
237	ix := New()
238	urequire.NoError(t, ix.Add("gno", 9))
239	urequire.NoError(t, ix.Add("gno", 4))
240
241	got, ok := ix.First("gno")
242	urequire.True(t, ok)
243	uassert.Equal(t, "4", got.String())
244
245	_, ok = ix.First("missing")
246	uassert.False(t, ok)
247}
248
249// Many ids under one key is the case this package exists for, so the binary
250// search has to hold at a size where a linear scan would still look fine.
251func TestManyIDsUnderOneKey(t *testing.T) {
252	ix := New()
253	for n := uint64(64); n >= 1; n-- {
254		urequire.NoError(t, ix.Add("gno", store.ID(n)))
255	}
256	uassert.Equal(t, 64, ix.Count("gno"))
257	uassert.Equal(t, 1, ix.Keys())
258
259	got := ix.Lookup("gno")
260	urequire.Equal(t, 64, len(got))
261	for i, id := range got {
262		uassert.Equal(t, store.ID(i+1).String(), id.String())
263	}
264}
265
266// maxInt is the largest int, which is where unchecked arithmetic in Page and
267// Pages goes wrong.
268const maxInt = int(^uint(0) >> 1)
269
270// A removed id must drop the old backing array, not merely shorten it.
271//
272// In gno a slice is ONE persisted object, and the deposit comes back when the
273// object is dropped and at no other time (EFFECTIVE_GNO.md section 2.12).
274// Shortening in place keeps the peak allocation charged until the whole key
275// goes, so a key that grew to 1,000 ids and shrank to 1 would still be paying
276// for 1,000.
277//
278// Not observable through the API, so this reads the entry directly: same
279// package, and the invariant is worth more than the encapsulation.
280func TestRemoveReleasesTheBackingArray(t *testing.T) {
281	ix := New()
282	for n := uint64(1); n <= 16; n++ {
283		urequire.NoError(t, ix.Add("gno", store.ID(n)))
284	}
285	for n := uint64(1); n <= 15; n++ {
286		uassert.True(t, ix.Remove("gno", store.ID(n)))
287	}
288
289	e, _ := ix.s.tree.Get("gno").(*entry)
290	urequire.True(t, e != nil)
291	uassert.Equal(t, 1, len(e.ids))
292	uassert.Equal(t, 1, cap(e.ids),
293		"the shrunk bucket still owns its peak allocation, so the deposit never comes back")
294}
295
296// Page's contract is that a page past the end is nil. Unchecked
297// (page-1)*size wraps negative at the limit, and bptree.IterateByOffset
298// normalises a negative offset to 0, so the caller silently gets page 1.
299func TestPageDoesNotWrapAtTheIntegerLimit(t *testing.T) {
300	ix := New()
301	for _, k := range []string{"a", "b", "c"} {
302		urequire.NoError(t, ix.Add(k, 1))
303	}
304	uassert.Equal(t, 0, len(ix.Page(maxInt, 2)),
305		"a page past the end is nil, including when the offset would overflow")
306	uassert.Equal(t, 0, len(ix.Page(maxInt/2+2, 2)))
307}
308
309// A huge size must not reserve a huge slice for three keys.
310func TestPageCapsItsAllocationToWhatExists(t *testing.T) {
311	ix := New()
312	for _, k := range []string{"a", "b", "c"} {
313		urequire.NoError(t, ix.Add(k, 1))
314	}
315	got := ix.Page(1, maxInt)
316	urequire.Equal(t, 3, len(got))
317	uassert.True(t, cap(got) <= 3, "the capacity follows the keys, not the requested size")
318}
319
320// Pages documents "at least 1". The ceiling n+size-1 wraps when size is the
321// limit, and the division then returns 0.
322func TestPagesNeverReturnsZeroAtTheIntegerLimit(t *testing.T) {
323	ix := New()
324	urequire.NoError(t, ix.Add("a", 1))
325	urequire.NoError(t, ix.Add("b", 1))
326	uassert.Equal(t, 1, ix.Pages(maxInt))
327}
328
329// An Index is a handle. A copy taken after the first write must see every
330// later write through either copy, or Len (a counter) and Keys (the tree)
331// disagree about what the index holds.
332func TestCopiesAfterTheFirstWriteShareOneIndex(t *testing.T) {
333	var a Index
334	urequire.NoError(t, a.Add("a", 1))
335	b := a
336	urequire.NoError(t, b.Add("b", 2))
337	uassert.Equal(t, 2, a.Keys())
338	uassert.Equal(t, 2, a.Len(), "the copy wrote a pair the original does not count")
339	uassert.True(t, b.Remove("a", 1))
340	uassert.Equal(t, 1, a.Len())
341
342	u := *Unique()
343	v := u
344	urequire.NoError(t, v.Add("k", 1))
345	uassert.ErrorIs(t, u.Add("k", 2), ErrDuplicateKey)
346	uassert.Equal(t, 1, u.Len())
347}
348
349// Every out-of-range page is nil, including the one that starts exactly at
350// the end and the first page of an index that has been emptied.
351func TestPagePastTheEndIsAlwaysNil(t *testing.T) {
352	ix := New()
353	for _, k := range []string{"a", "b", "c", "d"} {
354		urequire.NoError(t, ix.Add(k, 1))
355	}
356	uassert.True(t, ix.Page(2, 2) != nil)
357	uassert.True(t, ix.Page(3, 2) == nil, "a page starting exactly at the end is past the end")
358	uassert.True(t, ix.Page(4, 2) == nil)
359
360	for _, k := range []string{"a", "b", "c", "d"} {
361		uassert.Equal(t, 1, ix.RemoveKey(k))
362	}
363	uassert.True(t, ix.Page(1, 2) == nil, "an emptied index has no first page")
364
365	var zero Index
366	uassert.True(t, zero.Page(1, 2) == nil)
367}