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

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}