PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/duebook

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
duebook
Namespace
g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Files
3 (gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/duebook
gno
0.9

Files (3)

  • gnomod.tomltoml
  • duebook.gnogno
  • duebook_test.gnogno
duebook_test.gnogno
1package duebook23import "testing"45const (6	alice = "g1alice"7	bob   = "g1bob"8)910// 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 b18}1920// --- construction ---------------------------------------------------------2122func TestNewRejectsBadConfig(t *testing.T) {23	cases := []struct {24		name                string25		min, max            int6426		maxOpen             int27	}{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}4041func 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}5556// --- scheduling -----------------------------------------------------------5758func 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}7576func 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}9192func TestScheduleValidation(t *testing.T) {93	b := newBook(t)94	big := make([]byte, MaxPayloadLen+1)95	cases := []struct {96		name    string97		owner   string98		payload string99		now     int64100		delay   int64101		ttl     int64102		want    error103	}{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}124125func 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}143144func 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}160161// --- the central claim: exactly-once ---------------------------------------162163func TestClaimSucceedsExactlyOnce(t *testing.T) {164	b := newBook(t)165	id := b.MustSchedule(alice, "do-the-thing", 0, 10, 0)166167	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}180181func 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}198199func TestConsumedByAnyPathStaysConsumed(t *testing.T) {200	// Every terminal path must close the ID to all three transitions.201	paths := []struct {202		name    string203		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 60212		p.consume(b, id)213214		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}228229// --- the claim window -----------------------------------------------------230231func TestClaimWindowIsHalfOpen(t *testing.T) {232	// Schedule at 0, delay 10, ttl 5 => claimable on [10, 15).233	cases := []struct {234		now  int64235		want error236	}{237		{0, ErrNotDue},238		{9, ErrNotDue},239		{10, nil}, // due instant is claimable240		{14, nil},241		{15, ErrExpired}, // expiry instant is NOT claimable242		{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}259260func 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}270271func 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}277278// --- cancellation ---------------------------------------------------------279280func TestCancelOwnerOnly(t *testing.T) {281	b := newBook(t)282	id := b.MustSchedule(alice, "", 0, 10, 0)283284	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}297298func TestCancelWorksAfterDue(t *testing.T) {299	// Cancellation is allowed while OPEN, not only while pending — an owner300	// can still pull a deferral back after it became claimable, as long as301	// 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}308309func 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}317318// --- expiry ---------------------------------------------------------------319320func TestExpireRequiresExpiry(t *testing.T) {321	b := newBook(t)322	// Not yet expired.323	id := b.MustSchedule(alice, "", 0, 10, 5) // expires 15324	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	}330331	// 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}337338func 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}346347func TestExpiredDeferralCannotBeClaimedOrGriefTheCap(t *testing.T) {348	b, _ := New(0, 100, 1)349	id := b.MustSchedule(alice, "", 0, 10, 5) // expires 15350	// 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}363364// --- queries --------------------------------------------------------------365366func TestDueReturnsOnlyClaimableInIDOrder(t *testing.T) {367	b := newBook(t)368	b.MustSchedule(alice, "a", 0, 10, 0)  // 1: due 10369	b.MustSchedule(alice, "b", 0, 500, 0) // 2: due 500, not yet370	b.MustSchedule(alice, "c", 0, 10, 5)  // 3: due 10, expired at 15371	b.MustSchedule(alice, "d", 0, 20, 0)  // 4: due 20372373	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}381382func 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}397398func TestExpirable(t *testing.T) {399	b := newBook(t)400	b.MustSchedule(alice, "", 0, 10, 5) // 1: expires 15401	b.MustSchedule(alice, "", 0, 10, 0) // 2: never expires402	b.MustSchedule(alice, "", 0, 10, 5) // 3: expires 15403404	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}412413func TestIterateOpenIsIDOrdered(t *testing.T) {414	b, _ := New(0, 100, 32)415	// Schedule enough to cross the 1->2 digit boundary, where naive string416	// 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 := 0422	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.ID427		n++428		return false429	})430	if n != 12 {431		t.Errorf("iterated %d, want 12", n)432	}433}434435func 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 = bob441442	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}447448// --- helpers --------------------------------------------------------------449450func 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}466467func 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}480481func 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}493

Functions

not supported for pure packages by the node (vm/qfuncs)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.