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/moul/x/games/accrual/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
moul / x / games / accrual
Files
4 (README)(gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/moul/x/games/accrual/v0
gno
0.9

Files (4)

  • README.mdmarkdown
  • gnomod.tomltoml
  • accrual.gnogno
  • accrual_test.gnogno
accrual_test.gnogno
1package accrual23import (4	"testing"56	"gno.land/p/nt/uassert/v0"7)89func mustNew(t *testing.T, amount, period, capacity int64) Rate {10	t.Helper()11	r, err := New(amount, period, capacity)12	uassert.NoError(t, err)13	return r14}1516func TestNewValidation(t *testing.T) {17	cases := []struct {18		name                     string19		amount, period, capacity int6420		want                     error21	}{22		{"zero amount", 0, 60, 100, ErrBadAmount},23		{"negative amount", -1, 60, 100, ErrBadAmount},24		{"zero period", 1, 0, 100, ErrBadPeriod},25		{"negative period", 1, -60, 100, ErrBadPeriod},26		{"negative cap", 1, 60, -1, ErrBadCap},27		{"zero cap is unbounded", 1, 60, 0, nil},28		{"all positive", 5, 60, 100, nil},29	}30	for _, tc := range cases {31		_, err := New(tc.amount, tc.period, tc.capacity)32		if tc.want == nil {33			uassert.NoError(t, err, tc.name)34			continue35		}36		uassert.ErrorIs(t, err, tc.want, tc.name)37	}38}3940func TestAdvance(t *testing.T) {41	cases := []struct {42		name                     string43		amount, period, capacity int6444		stock, anchor, now       int6445		wantStock, wantAnchor    int6446	}{47		{"nothing elapsed", 1, 60, 0, 0, 1000, 1000, 0, 1000},48		{"less than one period", 1, 60, 0, 0, 1000, 1059, 0, 1000},49		{"exactly one period", 1, 60, 0, 0, 1000, 1060, 1, 1060},50		{"one period and change keeps the change", 1, 60, 0, 0, 1000, 1119, 1, 1060},51		{"ten periods", 3, 60, 0, 0, 1000, 1600, 30, 1600},52		{"adds to what was held", 3, 60, 0, 7, 1000, 1600, 37, 1600},53		{"saturates at the cap", 1, 60, 10, 0, 1000, 9000, 10, 8980},54		{"already full produces nothing", 1, 60, 10, 10, 1000, 9000, 10, 8980},55		{"over the cap is left alone", 1, 60, 10, 25, 1000, 9000, 25, 8980},56		{"uncapped keeps going", 1, 60, 0, 0, 0, 604800, 10080, 604800},57	}58	for _, tc := range cases {59		r := mustNew(t, tc.amount, tc.period, tc.capacity)60		gotStock, gotAnchor, err := r.Advance(tc.stock, tc.anchor, tc.now)61		uassert.NoError(t, err, tc.name)62		uassert.Equal(t, tc.wantStock, gotStock, tc.name+": stock")63		uassert.Equal(t, tc.wantAnchor, gotAnchor, tc.name+": anchor")64	}65}6667// TestAnchorKeepsTheRemainder pins the one decision the whole package rests68// on: the new anchor is not now. Advancing to a time halfway through a period69// leaves that half on the clock, which is what makes splitting free.70func TestAnchorKeepsTheRemainder(t *testing.T) {71	r := mustNew(t, 1, 60, 0)72	_, anchor, err := r.Advance(0, 0, 599)73	uassert.NoError(t, err)74	uassert.Equal(t, int64(540), anchor, "anchor must stop at the last whole period, not at now")75}7677// TestSplitInvariant is the contract of this package:78//79//	Advance(s, a, c) == Advance(Advance(s, a, b), b, c)80//81// It runs over pseudo-random partitions rather than hand-picked ones, because82// an implementation that re-anchors to now passes every round span and only83// fails on the ones that land mid-period. The generator is a fixed LCG, so a84// failure is reproducible.85func TestSplitInvariant(t *testing.T) {86	rates := []Rate{87		mustNew(t, 1, 60, 0),      // one a minute, unbounded88		mustNew(t, 7, 13, 0),      // coprime, so remainders never line up89		mustNew(t, 1, 60, 500),    // capped90		mustNew(t, 250, 3600, 0),  // coarse period91		mustNew(t, 1, 1, 0),       // degenerate: every tick pays92		mustNew(t, 3, 1000, 1234), // capped and coarse93	}9495	seed := uint64(0x9E3779B97F4A7C15)96	next := func(n int64) int64 {97		seed = seed*6364136223846793005 + 144269504088896340798		return int64(seed>>33) % n99	}100101	for _, r := range rates {102		for i := 0; i < 400; i++ {103			start := next(10000)104			span := next(100000)105			stock := next(600)106			end := start + span107108			wantStock, wantAnchor, err := r.Advance(stock, start, end)109			uassert.NoError(t, err)110111			// Cut the span into between one and six pieces and walk them.112			cuts := next(6) + 1113			gotStock, gotAnchor := stock, start114			at := start115			for c := int64(0); c < cuts; c++ {116				remaining := end - at117				if remaining <= 0 {118					break119				}120				step := next(remaining + 1)121				if c == cuts-1 {122					step = remaining123				}124				at += step125				gotStock, gotAnchor, err = r.Advance(gotStock, gotAnchor, at)126				uassert.NoError(t, err)127			}128			// Land on the end whatever the cuts did.129			gotStock, gotAnchor, err = r.Advance(gotStock, gotAnchor, end)130			uassert.NoError(t, err)131132			uassert.Equal(t, wantStock, gotStock, "split stock diverged")133			uassert.Equal(t, wantAnchor, gotAnchor, "split anchor diverged")134		}135	}136}137138// TestClaimingEveryTickIsNotFree is the regression for the bug this package139// exists to make unrepresentable. r/moul/x/daily/tamagotchi truncated its140// per-call decay, so acting every tick cost nothing and acting rarely cost141// everything. Here the two agree exactly.142func TestClaimingEveryTickIsNotFree(t *testing.T) {143	r := mustNew(t, 1, 60, 0)144	const span = 6000 // 100 whole periods145146	oneShot, _, err := r.Advance(0, 0, span)147	uassert.NoError(t, err)148149	for _, cadence := range []int64{1, 2, 7, 60, 61, 599, 3000} {150		stock, anchor := int64(0), int64(0)151		for at := cadence; at <= span; at += cadence {152			stock, anchor, err = r.Advance(stock, anchor, at)153			uassert.NoError(t, err)154		}155		stock, _, err = r.Advance(stock, anchor, span)156		uassert.NoError(t, err)157		uassert.Equal(t, oneShot, stock, "cadence changed the payout")158	}159}160161// TestAtMatchesAdvance pins that the read path cannot drift from the write162// path, which was the second and quieter half of the tamagotchi bug.163func TestAtMatchesAdvance(t *testing.T) {164	r := mustNew(t, 5, 17, 900)165	for now := int64(3); now < 4000; now += 37 {166		want, _, err := r.Advance(11, 3, now)167		uassert.NoError(t, err)168		got, err := r.At(11, 3, now)169		uassert.NoError(t, err)170		uassert.Equal(t, want, got, "At diverged from Advance")171	}172}173174func TestAdvanceRejects(t *testing.T) {175	r := mustNew(t, 1, 60, 0)176177	_, _, err := r.Advance(-1, 0, 100)178	uassert.ErrorIs(t, err, ErrNegativeStock)179180	_, _, err = r.Advance(0, 100, 99)181	uassert.ErrorIs(t, err, ErrBackwards)182183	// A Rate built by literal instead of New is still checked.184	_, _, err = Rate{}.Advance(0, 0, 100)185	uassert.ErrorIs(t, err, ErrBadAmount)186	_, _, err = Rate{Amount: 1}.Advance(0, 0, 100)187	uassert.ErrorIs(t, err, ErrBadPeriod)188	_, _, err = Rate{Amount: 1, Period: 1, Cap: -1}.Advance(0, 0, 100)189	uassert.ErrorIs(t, err, ErrBadCap)190}191192func TestAdvanceOverflow(t *testing.T) {193	// An uncapped rate that would pass int64 refuses rather than wrapping194	// into a negative stock, which is how a large vesting grant once195	// reported a negative balance.196	r := mustNew(t, maxInt64/2, 1, 0)197	_, _, err := r.Advance(0, 0, 100)198	uassert.ErrorIs(t, err, ErrOverflow)199200	// The same rate with a cap always has an answer, so it gives one201	// instead of an error, whichever of the two guards it trips.202	capped := mustNew(t, maxInt64/2, 1, 1000)203	got, anchor, err := capped.Advance(0, 0, 100)204	uassert.NoError(t, err)205	uassert.Equal(t, int64(1000), got)206	uassert.Equal(t, int64(100), anchor, "a saturated advance still moves the anchor")207208	got, _, err = capped.Advance(999, 0, 1)209	uassert.NoError(t, err)210	uassert.Equal(t, int64(1000), got, "the sum guard saturates too")211212	// A negative anchor far enough below now wraps the elapsed span itself.213	slow := mustNew(t, 1, maxInt64, 0)214	_, _, err = slow.Advance(0, -10, maxInt64-5)215	uassert.ErrorIs(t, err, ErrOverflow)216}217218func TestFull(t *testing.T) {219	cases := []struct {220		name                     string221		amount, period, capacity int64222		stock, anchor            int64223		want                     int64224	}{225		{"uncapped never fills", 1, 60, 0, 0, 1000, 0},226		{"already full is now", 1, 60, 10, 10, 1000, 1000},227		{"past full is now", 1, 60, 10, 99, 1000, 1000},228		{"empty takes the whole cap", 1, 60, 10, 0, 1000, 1600},229		{"partly full takes the rest", 1, 60, 10, 4, 1000, 1360},230		{"rounds up to the payout that crosses", 3, 60, 10, 0, 0, 240},231		{"exact division does not round up", 5, 60, 10, 0, 0, 120},232	}233	for _, tc := range cases {234		r := mustNew(t, tc.amount, tc.period, tc.capacity)235		got, err := r.Full(tc.stock, tc.anchor)236		uassert.NoError(t, err, tc.name)237		uassert.Equal(t, tc.want, got, tc.name)238	}239}240241// TestFullAgreesWithAdvance checks the two halves of the API against each242// other: at the time Full names, the stock is at the cap, and one period243// earlier it is not.244func TestFullAgreesWithAdvance(t *testing.T) {245	for _, r := range []Rate{246		mustNew(t, 1, 60, 10),247		mustNew(t, 3, 60, 10),248		mustNew(t, 7, 13, 1000),249		mustNew(t, 250, 3600, 875),250	} {251		full, err := r.Full(0, 0)252		uassert.NoError(t, err)253254		atFull, err := r.At(0, 0, full)255		uassert.NoError(t, err)256		uassert.Equal(t, r.Cap, atFull, "not full at the time Full named")257258		before, err := r.At(0, 0, full-r.Period)259		uassert.NoError(t, err)260		if before >= r.Cap {261			t.Errorf("already full a period early: %d >= %d", before, r.Cap)262		}263	}264}265266func TestFullRejects(t *testing.T) {267	r := mustNew(t, 1, 60, 10)268	_, err := r.Full(-1, 0)269	uassert.ErrorIs(t, err, ErrNegativeStock)270271	_, err = Rate{}.Full(0, 0)272	uassert.ErrorIs(t, err, ErrBadAmount)273274	far := mustNew(t, 1, maxInt64/2, 1000)275	_, err = far.Full(0, 0)276	uassert.ErrorIs(t, err, ErrOverflow)277}278

Functions

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

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