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.

duebook_test.gno

13.65 Kb · 492 lines
  1package duebook
  2
  3import "testing"
  4
  5const (
  6	alice = "g1alice"
  7	bob   = "g1bob"
  8)
  9
 10// newBook is the default fixture: delay in [10, 1000], 8 open slots.
 11func newBook(t *testing.T) *Book {
 12	t.Helper()
 13	b, err := New(10, 1000, 8)
 14	if err != nil {
 15		t.Fatalf("New: %v", err)
 16	}
 17	return b
 18}
 19
 20// --- construction ---------------------------------------------------------
 21
 22func TestNewRejectsBadConfig(t *testing.T) {
 23	cases := []struct {
 24		name                string
 25		min, max            int64
 26		maxOpen             int
 27	}{
 28		{"negative min", -1, 100, 4},
 29		{"max below min", 100, 99, 4},
 30		{"zero maxOpen", 0, 100, 0},
 31		{"negative maxOpen", 0, 100, -1},
 32		{"maxOpen over limit", 0, 100, MaxOpenLimit + 1},
 33	}
 34	for _, c := range cases {
 35		if _, err := New(c.min, c.max, c.maxOpen); err != ErrInvalidConfig {
 36			t.Errorf("%s: got %v, want ErrInvalidConfig", c.name, err)
 37		}
 38	}
 39}
 40
 41func TestNewAcceptsZeroMinDelay(t *testing.T) {
 42	b, err := New(0, 0, 1)
 43	if err != nil {
 44		t.Fatalf("New(0,0,1): %v", err)
 45	}
 46	id, err := b.Schedule(alice, "", 100, 0, 0)
 47	if err != nil {
 48		t.Fatalf("Schedule: %v", err)
 49	}
 50	// min == max == 0 means due immediately.
 51	if _, err := b.Claim(id, 100); err != nil {
 52		t.Fatalf("Claim at due instant: %v", err)
 53	}
 54}
 55
 56// --- scheduling -----------------------------------------------------------
 57
 58func TestScheduleComputesWindow(t *testing.T) {
 59	b := newBook(t)
 60	id, err := b.Schedule(alice, "payload", 1000, 50, 200)
 61	if err != nil {
 62		t.Fatalf("Schedule: %v", err)
 63	}
 64	d, ok := b.Get(id)
 65	if !ok {
 66		t.Fatal("Get: not found")
 67	}
 68	if d.CreatedAt != 1000 || d.DueAt != 1050 || d.ExpiresAt != 1250 {
 69		t.Errorf("window = (%d,%d,%d), want (1000,1050,1250)", d.CreatedAt, d.DueAt, d.ExpiresAt)
 70	}
 71	if d.Owner != alice || d.Payload != "payload" {
 72		t.Errorf("owner/payload = %q/%q", d.Owner, d.Payload)
 73	}
 74}
 75
 76func TestScheduleZeroTTLNeverExpires(t *testing.T) {
 77	b := newBook(t)
 78	id := b.MustSchedule(alice, "", 0, 10, 0)
 79	d, _ := b.Get(id)
 80	if d.ExpiresAt != 0 {
 81		t.Fatalf("ExpiresAt = %d, want 0", d.ExpiresAt)
 82	}
 83	if d.IsExpired(1 << 62) {
 84		t.Error("ttl==0 deferral reported expired")
 85	}
 86	// Still claimable in the far future.
 87	if _, err := b.Claim(id, 1<<62); err != nil {
 88		t.Fatalf("Claim far future: %v", err)
 89	}
 90}
 91
 92func TestScheduleValidation(t *testing.T) {
 93	b := newBook(t)
 94	big := make([]byte, MaxPayloadLen+1)
 95	cases := []struct {
 96		name    string
 97		owner   string
 98		payload string
 99		now     int64
100		delay   int64
101		ttl     int64
102		want    error
103	}{
104		{"empty owner", "", "", 0, 10, 0, ErrEmptyOwner},
105		{"payload too big", alice, string(big), 0, 10, 0, ErrPayloadTooBig},
106		{"negative now", alice, "", -1, 10, 0, ErrInvalidNow},
107		{"delay below min", alice, "", 0, 9, 0, ErrInvalidDelay},
108		{"delay above max", alice, "", 0, 1001, 0, ErrInvalidDelay},
109		{"negative delay", alice, "", 0, -5, 0, ErrInvalidDelay},
110		{"negative ttl", alice, "", 0, 10, -1, ErrInvalidTTL},
111	}
112	for _, c := range cases {
113		if _, err := b.Schedule(c.owner, c.payload, c.now, c.delay, c.ttl); err != c.want {
114			t.Errorf("%s: got %v, want %v", c.name, err, c.want)
115		}
116	}
117	if b.OpenCount() != 0 {
118		t.Errorf("failed Schedules mutated the book: OpenCount = %d", b.OpenCount())
119	}
120	if b.NextID() != 1 {
121		t.Errorf("failed Schedules consumed IDs: NextID = %d", b.NextID())
122	}
123}
124
125func TestScheduleOverflowGuard(t *testing.T) {
126	b, err := New(0, 1<<62, 4)
127	if err != nil {
128		t.Fatalf("New: %v", err)
129	}
130	const huge = int64(1<<63 - 1)
131	// now + delay overflows.
132	if _, err := b.Schedule(alice, "", huge-10, 1<<62, 0); err != ErrOverflow {
133		t.Errorf("dueAt overflow: got %v, want ErrOverflow", err)
134	}
135	// dueAt + ttl overflows.
136	if _, err := b.Schedule(alice, "", huge-1<<62, 1<<62, huge); err != ErrOverflow {
137		t.Errorf("expiresAt overflow: got %v, want ErrOverflow", err)
138	}
139	if b.OpenCount() != 0 {
140		t.Errorf("overflow left state behind: OpenCount = %d", b.OpenCount())
141	}
142}
143
144func TestScheduleRespectsCap(t *testing.T) {
145	b, _ := New(0, 100, 2)
146	b.MustSchedule(alice, "", 0, 10, 0)
147	b.MustSchedule(alice, "", 0, 10, 0)
148	if _, err := b.Schedule(alice, "", 0, 10, 0); err != ErrBookFull {
149		t.Fatalf("third Schedule: got %v, want ErrBookFull", err)
150	}
151	// Consuming one frees exactly one slot.
152	b.MustClaim(1, 10)
153	if _, err := b.Schedule(alice, "", 0, 10, 0); err != nil {
154		t.Fatalf("Schedule after Claim: %v", err)
155	}
156	if _, err := b.Schedule(alice, "", 0, 10, 0); err != ErrBookFull {
157		t.Fatalf("cap not re-applied: got %v, want ErrBookFull", err)
158	}
159}
160
161// --- the central claim: exactly-once ---------------------------------------
162
163func TestClaimSucceedsExactlyOnce(t *testing.T) {
164	b := newBook(t)
165	id := b.MustSchedule(alice, "do-the-thing", 0, 10, 0)
166
167	d, err := b.Claim(id, 10)
168	if err != nil {
169		t.Fatalf("first Claim: %v", err)
170	}
171	if d.Payload != "do-the-thing" {
172		t.Errorf("payload = %q", d.Payload)
173	}
174	for i := 0; i < 5; i++ {
175		if _, err := b.Claim(id, 10); err != ErrNotFound {
176			t.Fatalf("replay %d: got %v, want ErrNotFound", i, err)
177		}
178	}
179}
180
181func TestIDsAreNeverReused(t *testing.T) {
182	b := newBook(t)
183	id1 := b.MustSchedule(alice, "", 0, 10, 0)
184	b.MustClaim(id1, 10)
185	if b.OpenCount() != 0 {
186		t.Fatalf("OpenCount = %d after claim", b.OpenCount())
187	}
188	// The book is empty again, but the next ID must still move forward.
189	id2 := b.MustSchedule(alice, "", 0, 10, 0)
190	if id2 == id1 {
191		t.Fatalf("ID %d reused after the book emptied", id2)
192	}
193	// So the old ID stays permanently unclaimable.
194	if _, err := b.Claim(id1, 10); err != ErrNotFound {
195		t.Errorf("old ID claimable again: %v", err)
196	}
197}
198
199func TestConsumedByAnyPathStaysConsumed(t *testing.T) {
200	// Every terminal path must close the ID to all three transitions.
201	paths := []struct {
202		name    string
203		consume func(b *Book, id uint64)
204	}{
205		{"claim", func(b *Book, id uint64) { b.MustClaim(id, 10) }},
206		{"cancel", func(b *Book, id uint64) { b.MustCancel(id, alice) }},
207		{"expire", func(b *Book, id uint64) { b.MustExpire(id, 100) }},
208	}
209	for _, p := range paths {
210		b := newBook(t)
211		id := b.MustSchedule(alice, "", 0, 10, 50) // due 10, expires 60
212		p.consume(b, id)
213
214		if _, err := b.Claim(id, 10); err != ErrNotFound {
215			t.Errorf("%s then Claim: got %v, want ErrNotFound", p.name, err)
216		}
217		if _, err := b.Cancel(id, alice); err != ErrNotFound {
218			t.Errorf("%s then Cancel: got %v, want ErrNotFound", p.name, err)
219		}
220		if _, err := b.Expire(id, 100); err != ErrNotFound {
221			t.Errorf("%s then Expire: got %v, want ErrNotFound", p.name, err)
222		}
223		if _, ok := b.Get(id); ok {
224			t.Errorf("%s: deferral still visible", p.name)
225		}
226	}
227}
228
229// --- the claim window -----------------------------------------------------
230
231func TestClaimWindowIsHalfOpen(t *testing.T) {
232	// Schedule at 0, delay 10, ttl 5 => claimable on [10, 15).
233	cases := []struct {
234		now  int64
235		want error
236	}{
237		{0, ErrNotDue},
238		{9, ErrNotDue},
239		{10, nil}, // due instant is claimable
240		{14, nil},
241		{15, ErrExpired}, // expiry instant is NOT claimable
242		{16, ErrExpired},
243	}
244	for _, c := range cases {
245		b := newBook(t)
246		id := b.MustSchedule(alice, "", 0, 10, 5)
247		_, err := b.Claim(id, c.now)
248		if err != c.want {
249			t.Errorf("Claim at now=%d: got %v, want %v", c.now, err, c.want)
250		}
251		// A refused Claim must not consume the deferral.
252		if c.want != nil {
253			if _, ok := b.Get(id); !ok {
254				t.Errorf("refused Claim at now=%d consumed the deferral", c.now)
255			}
256		}
257	}
258}
259
260func TestClaimRejectsNegativeNow(t *testing.T) {
261	b := newBook(t)
262	id := b.MustSchedule(alice, "", 0, 10, 0)
263	if _, err := b.Claim(id, -1); err != ErrInvalidNow {
264		t.Fatalf("got %v, want ErrInvalidNow", err)
265	}
266	if _, ok := b.Get(id); !ok {
267		t.Error("negative now consumed the deferral")
268	}
269}
270
271func TestClaimUnknownID(t *testing.T) {
272	b := newBook(t)
273	if _, err := b.Claim(999, 10); err != ErrNotFound {
274		t.Fatalf("got %v, want ErrNotFound", err)
275	}
276}
277
278// --- cancellation ---------------------------------------------------------
279
280func TestCancelOwnerOnly(t *testing.T) {
281	b := newBook(t)
282	id := b.MustSchedule(alice, "", 0, 10, 0)
283
284	if _, err := b.Cancel(id, bob); err != ErrNotOwner {
285		t.Fatalf("bob cancelling alice's deferral: got %v, want ErrNotOwner", err)
286	}
287	if _, ok := b.Get(id); !ok {
288		t.Fatal("refused Cancel consumed the deferral")
289	}
290	if _, err := b.Cancel(id, ""); err != ErrEmptyOwner {
291		t.Errorf("empty owner: got %v, want ErrEmptyOwner", err)
292	}
293	if _, err := b.Cancel(id, alice); err != nil {
294		t.Fatalf("owner cancel: %v", err)
295	}
296}
297
298func TestCancelWorksAfterDue(t *testing.T) {
299	// Cancellation is allowed while OPEN, not only while pending — an owner
300	// can still pull a deferral back after it became claimable, as long as
301	// nobody claimed it first.
302	b := newBook(t)
303	id := b.MustSchedule(alice, "", 0, 10, 0)
304	if _, err := b.Cancel(id, alice); err != nil {
305		t.Fatalf("cancel after due: %v", err)
306	}
307}
308
309func TestCancelLosesRaceToClaim(t *testing.T) {
310	b := newBook(t)
311	id := b.MustSchedule(alice, "", 0, 10, 0)
312	b.MustClaim(id, 10)
313	if _, err := b.Cancel(id, alice); err != ErrNotFound {
314		t.Fatalf("cancel after claim: got %v, want ErrNotFound", err)
315	}
316}
317
318// --- expiry ---------------------------------------------------------------
319
320func TestExpireRequiresExpiry(t *testing.T) {
321	b := newBook(t)
322	// Not yet expired.
323	id := b.MustSchedule(alice, "", 0, 10, 5) // expires 15
324	if _, err := b.Expire(id, 14); err != ErrNotExpired {
325		t.Errorf("at 14: got %v, want ErrNotExpired", err)
326	}
327	if _, err := b.Expire(id, 15); err != nil {
328		t.Errorf("at 15: %v", err)
329	}
330
331	// ttl == 0 can never be expired away.
332	never := b.MustSchedule(alice, "", 0, 10, 0)
333	if _, err := b.Expire(never, 1<<62); err != ErrNotExpired {
334		t.Errorf("no-expiry deferral: got %v, want ErrNotExpired", err)
335	}
336}
337
338func TestExpireIsPermissionless(t *testing.T) {
339	b := newBook(t)
340	id := b.MustSchedule(alice, "", 0, 10, 5)
341	// No owner argument at all — anyone may clear a dead entry.
342	if _, err := b.Expire(id, 20); err != nil {
343		t.Fatalf("Expire: %v", err)
344	}
345}
346
347func TestExpiredDeferralCannotBeClaimedOrGriefTheCap(t *testing.T) {
348	b, _ := New(0, 100, 1)
349	id := b.MustSchedule(alice, "", 0, 10, 5) // expires 15
350	// Cap is full and the entry is dead.
351	if _, err := b.Schedule(bob, "", 20, 10, 0); err != ErrBookFull {
352		t.Fatalf("got %v, want ErrBookFull", err)
353	}
354	if _, err := b.Claim(id, 20); err != ErrExpired {
355		t.Fatalf("got %v, want ErrExpired", err)
356	}
357	// Anyone can unstick it.
358	b.MustExpire(id, 20)
359	if _, err := b.Schedule(bob, "", 20, 10, 0); err != nil {
360		t.Fatalf("Schedule after Expire: %v", err)
361	}
362}
363
364// --- queries --------------------------------------------------------------
365
366func TestDueReturnsOnlyClaimableInIDOrder(t *testing.T) {
367	b := newBook(t)
368	b.MustSchedule(alice, "a", 0, 10, 0)  // 1: due 10
369	b.MustSchedule(alice, "b", 0, 500, 0) // 2: due 500, not yet
370	b.MustSchedule(alice, "c", 0, 10, 5)  // 3: due 10, expired at 15
371	b.MustSchedule(alice, "d", 0, 20, 0)  // 4: due 20
372
373	got := b.Due(20, 10)
374	if len(got) != 2 {
375		t.Fatalf("Due returned %d entries, want 2", len(got))
376	}
377	if got[0].ID != 1 || got[1].ID != 4 {
378		t.Errorf("IDs = %d,%d; want 1,4", got[0].ID, got[1].ID)
379	}
380}
381
382func TestDueRespectsLimit(t *testing.T) {
383	b := newBook(t)
384	for i := 0; i < 5; i++ {
385		b.MustSchedule(alice, "", 0, 10, 0)
386	}
387	if got := b.Due(10, 2); len(got) != 2 {
388		t.Errorf("limit 2 returned %d", len(got))
389	}
390	if got := b.Due(10, 0); len(got) != 0 {
391		t.Errorf("limit 0 returned %d", len(got))
392	}
393	if got := b.Due(10, -1); len(got) != 0 {
394		t.Errorf("limit -1 returned %d", len(got))
395	}
396}
397
398func TestExpirable(t *testing.T) {
399	b := newBook(t)
400	b.MustSchedule(alice, "", 0, 10, 5) // 1: expires 15
401	b.MustSchedule(alice, "", 0, 10, 0) // 2: never expires
402	b.MustSchedule(alice, "", 0, 10, 5) // 3: expires 15
403
404	got := b.Expirable(15, 10)
405	if len(got) != 2 || got[0].ID != 1 || got[1].ID != 3 {
406		t.Fatalf("Expirable = %v", got)
407	}
408	if len(b.Expirable(14, 10)) != 0 {
409		t.Error("Expirable before expiry returned entries")
410	}
411}
412
413func TestIterateOpenIsIDOrdered(t *testing.T) {
414	b, _ := New(0, 100, 32)
415	// Schedule enough to cross the 1->2 digit boundary, where naive string
416	// keys would order "10" before "9".
417	for i := 0; i < 12; i++ {
418		b.MustSchedule(alice, "", 0, 1, 0)
419	}
420	prev := uint64(0)
421	n := 0
422	b.IterateOpen(func(d Deferral) bool {
423		if d.ID <= prev {
424			t.Fatalf("out of order: %d after %d", d.ID, prev)
425		}
426		prev = d.ID
427		n++
428		return false
429	})
430	if n != 12 {
431		t.Errorf("iterated %d, want 12", n)
432	}
433}
434
435func TestGetReturnsACopy(t *testing.T) {
436	b := newBook(t)
437	id := b.MustSchedule(alice, "original", 0, 10, 0)
438	d, _ := b.Get(id)
439	d.Payload = "tampered"
440	d.Owner = bob
441
442	again, _ := b.Get(id)
443	if again.Payload != "original" || again.Owner != alice {
444		t.Fatalf("mutating a returned Deferral changed the book: %+v", again)
445	}
446}
447
448// --- helpers --------------------------------------------------------------
449
450func TestIDKeyOrdering(t *testing.T) {
451	if idKey(9) >= idKey(10) {
452		t.Errorf("idKey(9)=%q not < idKey(10)=%q", idKey(9), idKey(10))
453	}
454	if len(idKey(1)) != 20 {
455		t.Errorf("idKey(1) width = %d, want 20", len(idKey(1)))
456	}
457	// Largest uint64 is exactly 20 digits and must not be truncated.
458	const maxU64 = ^uint64(0)
459	if got := idKey(maxU64); got != "18446744073709551615" {
460		t.Errorf("idKey(max) = %q", got)
461	}
462	if idKey(maxU64-1) >= idKey(maxU64) {
463		t.Error("ordering breaks at the top of the range")
464	}
465}
466
467func TestCheckedAdd(t *testing.T) {
468	const max = int64(1<<63 - 1)
469	const min = int64(-1 << 63)
470	if _, ok := checkedAdd(max, 1); ok {
471		t.Error("max+1 reported ok")
472	}
473	if _, ok := checkedAdd(min, -1); ok {
474		t.Error("min-1 reported ok")
475	}
476	if v, ok := checkedAdd(5, 7); !ok || v != 12 {
477		t.Errorf("5+7 = %d,%v", v, ok)
478	}
479}
480
481func TestDeferralPredicates(t *testing.T) {
482	d := Deferral{DueAt: 10, ExpiresAt: 20}
483	if d.IsDue(9) || !d.IsDue(10) {
484		t.Error("IsDue boundary")
485	}
486	if d.IsExpired(19) || !d.IsExpired(20) {
487		t.Error("IsExpired boundary")
488	}
489	if d.IsClaimable(9) || !d.IsClaimable(10) || !d.IsClaimable(19) || d.IsClaimable(20) {
490		t.Error("IsClaimable window")
491	}
492}