Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

fenwick_test.gno

9.63 Kb · 350 lines
  1package fenwick
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/nt/uassert/v0"
  7)
  8
  9// naive is the reference implementation every structural test is checked
 10// against: the obvious O(n) loop nobody would ship. A data structure whose
 11// whole claim is "same answers, better complexity" is only worth trusting if
 12// the same answers half is pinned against something too simple to be wrong.
 13type naive []int64
 14
 15func (s naive) prefix(i int) int64 {
 16	if i > len(s) {
 17		i = len(s)
 18	}
 19	var sum int64
 20	for k := 0; k < i; k++ {
 21		sum += s[k]
 22	}
 23	return sum
 24}
 25
 26func (s naive) searchPrefix(target int64) int {
 27	if target < 0 {
 28		return 0
 29	}
 30	var run int64
 31	for i, v := range s {
 32		run += v
 33		if run > target {
 34			return i
 35		}
 36	}
 37	return len(s)
 38}
 39
 40func TestPrefixMatchesNaive(t *testing.T) {
 41	cases := []struct {
 42		name string
 43		vals []int64
 44	}{
 45		{"empty", []int64{}},
 46		{"one", []int64{7}},
 47		{"two", []int64{3, 4}},
 48		// A power of two and one past it: the tree's shape changes at the
 49		// boundary, and an off-by-one in lowBit only shows on one side of it.
 50		{"eight", []int64{1, 2, 3, 4, 5, 6, 7, 8}},
 51		{"nine", []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}},
 52		{"zeros", []int64{0, 0, 0, 0, 0}},
 53		{"negatives", []int64{5, -3, 2, -10, 40}},
 54		{"large", []int64{1 << 40, 1 << 40, -(1 << 41)}},
 55	}
 56	for _, tc := range cases {
 57		t.Run(tc.name, func(t *testing.T) {
 58			tr := FromSlice(tc.vals)
 59			ref := naive(tc.vals)
 60			uassert.Equal(t, len(tc.vals), tr.Len())
 61			for i := 0; i <= len(tc.vals); i++ {
 62				uassert.Equal(t, ref.prefix(i), tr.Prefix(i))
 63			}
 64			for i := range tc.vals {
 65				uassert.Equal(t, tc.vals[i], tr.At(i))
 66			}
 67			uassert.Equal(t, ref.prefix(len(tc.vals)), tr.Total())
 68		})
 69	}
 70}
 71
 72// TestFromSliceEqualsRepeatedAdd pins the O(n) construction against the O(n log
 73// n) one. They are different code paths writing the same array, and only this
 74// says so.
 75func TestFromSliceEqualsRepeatedAdd(t *testing.T) {
 76	vals := []int64{4, 0, 9, 2, 7, 1, 1, 6, 3}
 77
 78	built := FromSlice(vals)
 79	added := New(len(vals))
 80	for i, v := range vals {
 81		added.Add(i, v)
 82	}
 83
 84	uassert.Equal(t, len(built.t), len(added.t))
 85	for i := range built.t {
 86		uassert.Equal(t, built.t[i], added.t[i])
 87	}
 88}
 89
 90func TestRange(t *testing.T) {
 91	vals := []int64{1, 2, 3, 4, 5}
 92	tr := FromSlice(vals)
 93
 94	cases := []struct {
 95		name   string
 96		lo, hi int
 97		want   int64
 98	}{
 99		{"whole", 0, 5, 15},
100		{"middle", 1, 4, 9},
101		{"single", 2, 3, 3},
102		{"empty", 2, 2, 0},
103		{"inverted", 4, 1, 0},
104		{"low clamped", -100, 2, 3},
105		{"high clamped", 3, 100, 9},
106		{"both clamped", -7, 700, 15},
107		{"wholly below", -9, -2, 0},
108		{"wholly above", 60, 90, 0},
109	}
110	for _, tc := range cases {
111		t.Run(tc.name, func(t *testing.T) {
112			uassert.Equal(t, tc.want, tr.Range(tc.lo, tc.hi))
113		})
114	}
115}
116
117// TestReadsAreTotal is the package's load-bearing safety claim: no read panics,
118// whatever it is handed. A realm renders with these, a live realm cannot be
119// redeployed, so a panicking read is a permanently broken page.
120func TestReadsAreTotal(t *testing.T) {
121	for _, n := range []int{0, 1, 5} {
122		tr := New(n)
123		for i := 0; i < n; i++ {
124			tr.Add(i, int64(i+1))
125		}
126		for _, i := range []int{-1 << 30, -1, 0, n, n + 1, 1 << 30} {
127			tr.Prefix(i)
128			tr.At(i)
129			tr.Range(i, i+1)
130			tr.Covers(i)
131			tr.SearchPrefix(int64(i))
132		}
133		tr.Total()
134		tr.Slice()
135	}
136}
137
138func TestAtOutOfRangeIsZero(t *testing.T) {
139	tr := FromSlice([]int64{1, 2, 3})
140	uassert.Equal(t, int64(0), tr.At(-1))
141	uassert.Equal(t, int64(0), tr.At(3))
142	uassert.Equal(t, int64(0), tr.At(1<<20))
143}
144
145func TestPrefixClampsRatherThanPanics(t *testing.T) {
146	tr := FromSlice([]int64{1, 2, 3})
147	uassert.Equal(t, int64(0), tr.Prefix(-5))
148	uassert.Equal(t, int64(6), tr.Prefix(99))
149}
150
151func TestAddAndSet(t *testing.T) {
152	tr := FromSlice([]int64{1, 2, 3, 4})
153
154	tr.Add(1, 10)
155	uassert.Equal(t, int64(12), tr.At(1))
156	uassert.Equal(t, int64(20), tr.Total())
157
158	old := tr.Set(1, 2)
159	uassert.Equal(t, int64(12), old)
160	uassert.Equal(t, int64(2), tr.At(1))
161	uassert.Equal(t, int64(10), tr.Total())
162
163	tr.Add(3, -4)
164	uassert.Equal(t, int64(0), tr.At(3))
165	uassert.Equal(t, int64(6), tr.Total())
166}
167
168// TestWritesArePanicking is the other half of the read/write asymmetry. A
169// write is a transaction and should abort; a read is a render and must not.
170func TestWritesArePanicking(cur realm, t *testing.T) {
171	tr := New(3)
172	uassert.PanicsWithMessage(t, cur, "fenwick: index out of range", func() { tr.Add(3, 1) })
173	uassert.PanicsWithMessage(t, cur, "fenwick: index out of range", func() { tr.Add(-1, 1) })
174	uassert.PanicsWithMessage(t, cur, "fenwick: index out of range", func() { tr.Set(9, 1) })
175	uassert.PanicsWithMessage(t, cur, "fenwick: negative size", func() { New(-1) })
176	uassert.PanicsWithMessage(t, cur, "fenwick: size above MaxSize", func() { New(MaxSize + 1) })
177	uassert.NotPanics(t, cur, func() { New(MaxSize) })
178}
179
180func TestSearchPrefixMatchesNaive(t *testing.T) {
181	cases := []struct {
182		name string
183		vals []int64
184	}{
185		{"empty", []int64{}},
186		{"flat", []int64{1, 1, 1, 1}},
187		// Zero weights must be unselectable: this is the property a weighted
188		// draw depends on, and the one a naive scan gets wrong by returning the
189		// first index whose running total is merely >= target.
190		{"with zeros", []int64{0, 3, 0, 0, 2, 0}},
191		{"uneven", []int64{5, 0, 1, 9, 2}},
192		{"leading zero", []int64{0, 0, 7}},
193		{"eight", []int64{1, 2, 3, 4, 5, 6, 7, 8}},
194		{"nine", []int64{9, 1, 1, 1, 1, 1, 1, 1, 1}},
195	}
196	for _, tc := range cases {
197		t.Run(tc.name, func(t *testing.T) {
198			tr := FromSlice(tc.vals)
199			ref := naive(tc.vals)
200			total := ref.prefix(len(tc.vals))
201			// Probe every reachable target plus both sides of the boundary.
202			for target := int64(-2); target <= total+2; target++ {
203				uassert.Equal(t, ref.searchPrefix(target), tr.SearchPrefix(target))
204			}
205		})
206	}
207}
208
209// TestSearchPrefixNeverSelectsAZeroWeight states the draw property directly,
210// rather than leaving it implied by agreement with the reference.
211func TestSearchPrefixNeverSelectsAZeroWeight(t *testing.T) {
212	vals := []int64{0, 5, 0, 0, 3, 0, 2, 0}
213	tr := FromSlice(vals)
214	for target := int64(0); target < tr.Total(); target++ {
215		got := tr.SearchPrefix(target)
216		if vals[got] == 0 {
217			t.Errorf("target %d selected slot %d, which has weight 0", target, got)
218		}
219	}
220	// Every draw lands inside the tree, and only an exhausted target runs off
221	// the end.
222	uassert.Equal(t, tr.Len(), tr.SearchPrefix(tr.Total()))
223}
224
225// TestSearchPrefixIsProportional checks the distribution, not just the bounds:
226// slot i must own exactly vals[i] of the Total targets.
227func TestSearchPrefixIsProportional(t *testing.T) {
228	vals := []int64{3, 1, 0, 6, 2}
229	tr := FromSlice(vals)
230	hits := make([]int64, len(vals))
231	for target := int64(0); target < tr.Total(); target++ {
232		hits[tr.SearchPrefix(target)]++
233	}
234	for i := range vals {
235		uassert.Equal(t, vals[i], hits[i])
236	}
237}
238
239func TestSlice(t *testing.T) {
240	vals := []int64{6, 0, -2, 11}
241	tr := FromSlice(vals)
242	got := tr.Slice()
243	uassert.Equal(t, len(vals), len(got))
244	for i := range vals {
245		uassert.Equal(t, vals[i], got[i])
246	}
247	uassert.Equal(t, 0, len(New(0).Slice()))
248}
249
250// TestFromSliceCopies pins that the tree does not alias the caller's slice.
251func TestFromSliceCopies(t *testing.T) {
252	vals := []int64{1, 2, 3}
253	tr := FromSlice(vals)
254	vals[0] = 999
255	uassert.Equal(t, int64(1), tr.At(0))
256}
257
258func TestCovers(t *testing.T) {
259	tr := New(8)
260	cases := []struct {
261		name   string
262		i      int
263		lo, hi int
264	}{
265		{"odd covers one", 1, 0, 1},
266		{"two covers two", 2, 0, 2},
267		{"three covers one", 3, 2, 3},
268		{"four covers four", 4, 0, 4},
269		{"six covers two", 6, 4, 6},
270		{"eight covers all", 8, 0, 8},
271		{"below range", 0, 0, 0},
272		{"above range", 9, 0, 0},
273	}
274	for _, tc := range cases {
275		t.Run(tc.name, func(t *testing.T) {
276			lo, hi := tr.Covers(tc.i)
277			uassert.Equal(t, tc.lo, lo)
278			uassert.Equal(t, tc.hi, hi)
279		})
280	}
281}
282
283// TestCoversPartitions is the structural claim behind the whole package: the
284// nodes a Prefix walk visits tile [0, i) exactly, with no gap and no overlap.
285// If that ever stops holding, every sum is wrong and this says so in one place.
286func TestCoversPartitions(t *testing.T) {
287	const n = 13
288	tr := New(n)
289	for i := 1; i <= n; i++ {
290		covered := make([]bool, n)
291		for j := i; j > 0; j -= lowBit(j) {
292			lo, hi := tr.Covers(j)
293			for k := lo; k < hi; k++ {
294				if covered[k] {
295					t.Errorf("prefix %d: slot %d covered twice", i, k)
296				}
297				covered[k] = true
298			}
299		}
300		for k := 0; k < n; k++ {
301			if covered[k] != (k < i) {
302				t.Errorf("prefix %d: slot %d covered=%t, want %t", i, k, covered[k], k < i)
303			}
304		}
305	}
306}
307
308func TestLowBit(t *testing.T) {
309	cases := []struct{ in, want int }{
310		{1, 1}, {2, 2}, {3, 1}, {4, 4}, {6, 2}, {8, 8}, {12, 4}, {96, 32},
311	}
312	for _, tc := range cases {
313		uassert.Equal(t, tc.want, lowBit(tc.in))
314	}
315}
316
317func TestHighBit(t *testing.T) {
318	cases := []struct{ in, want int }{
319		{0, 0}, {1, 1}, {2, 2}, {3, 2}, {4, 4}, {7, 4}, {8, 8}, {9, 8}, {1000, 512},
320	}
321	for _, tc := range cases {
322		uassert.Equal(t, tc.want, highBit(tc.in))
323	}
324}
325
326// TestLargeTreeStaysConsistent exercises a size past the small hand-checked
327// cases, where a wrong jump in SearchPrefix would still look plausible.
328func TestLargeTreeStaysConsistent(t *testing.T) {
329	const n = 500
330	vals := make([]int64, n)
331	// Deterministic, and deliberately not uniform: a constant weight hides a
332	// SearchPrefix that is off by a slot.
333	for i := range vals {
334		vals[i] = int64((i*7919)%13) + 1
335	}
336	tr := FromSlice(vals)
337	ref := naive(vals)
338
339	uassert.Equal(t, ref.prefix(n), tr.Total())
340	for _, i := range []int{0, 1, 63, 64, 65, 255, 256, 257, n - 1, n} {
341		uassert.Equal(t, ref.prefix(i), tr.Prefix(i))
342	}
343	for _, target := range []int64{0, 1, 100, 1000, tr.Total() - 1, tr.Total()} {
344		uassert.Equal(t, ref.searchPrefix(target), tr.SearchPrefix(target))
345	}
346
347	tr.Add(499, 1000)
348	uassert.Equal(t, ref.prefix(n)+1000, tr.Total())
349	uassert.Equal(t, vals[499]+1000, tr.At(499))
350}