streak_test.gno
4.90 Kb · 179 lines
1package streak
2
3import (
4 "strconv"
5 "strings"
6 "testing"
7
8 "chain/runtime"
9
10 "gno.land/p/nt/avl/v0"
11 "gno.land/p/nt/testutils/v0"
12 "gno.land/p/nt/uassert/v0"
13)
14
15// reset clears realm state between tests.
16func reset() { users = avl.NewTree() }
17
18// alignDay moves to the start of the next check-in window, so a test begins on
19// a clean day boundary. testing only exposes a RELATIVE SkipHeights (there is
20// no absolute height setter, and no testing.Height), so days are driven
21// forward from wherever the previous test left the chain — never assume an
22// absolute day number, ask Day().
23func alignDay() {
24 if rem := runtime.ChainHeight() % BlocksPerDay; rem != 0 {
25 testing.SkipHeights(BlocksPerDay - rem)
26 }
27}
28
29// advanceDays moves the chain forward by n whole windows.
30func advanceDays(n int64) { testing.SkipHeights(n * BlocksPerDay) }
31
32// checkIn runs a check-in as addr.
33func checkIn(cur realm, addr address) int {
34 testing.SetRealm(testing.NewUserRealm(addr))
35 return CheckIn(cross(cur))
36}
37
38func TestFirstCheckInStartsAtOne(cur realm, t *testing.T) {
39 reset()
40 alignDay()
41 alice := testutils.TestAddress("alice")
42
43 uassert.Equal(t, 1, checkIn(cur, alice))
44 uassert.Equal(t, 1, Current(alice))
45 uassert.Equal(t, 1, Best(alice))
46}
47
48func TestConsecutiveDaysExtendStreak(cur realm, t *testing.T) {
49 reset()
50 alignDay()
51 alice := testutils.TestAddress("alice")
52
53 uassert.Equal(t, 1, checkIn(cur, alice))
54 advanceDays(1)
55 uassert.Equal(t, 2, checkIn(cur, alice))
56 advanceDays(1)
57 uassert.Equal(t, 3, checkIn(cur, alice))
58 uassert.Equal(t, 3, Best(alice))
59}
60
61// The whole point of the realm: a missed window restarts the run at 1, but the
62// record survives.
63func TestMissedDayResetsCurrentButKeepsBest(cur realm, t *testing.T) {
64 reset()
65 alignDay()
66 alice := testutils.TestAddress("alice")
67
68 checkIn(cur, alice)
69 advanceDays(1)
70 checkIn(cur, alice)
71 uassert.Equal(t, 2, Best(alice))
72
73 advanceDays(7) // a long gap
74 uassert.Equal(t, 1, checkIn(cur, alice))
75 uassert.Equal(t, 1, Current(alice))
76 uassert.Equal(t, 2, Best(alice))
77}
78
79func TestTwiceInSameDayPanics(cur realm, t *testing.T) {
80 reset()
81 alignDay()
82 alice := testutils.TestAddress("alice")
83 checkIn(cur, alice)
84
85 testing.SetRealm(testing.NewUserRealm(alice))
86 rec := revive(func() { CheckIn(cross(cur)) })
87 uassert.True(t, rec != nil)
88}
89
90// Current() reports the live truth: once the window lapses the streak is 0,
91// even though the stored record still holds the old value until the next
92// check-in rewrites it.
93func TestCurrentGoesStaleWithoutACheckIn(cur realm, t *testing.T) {
94 reset()
95 alignDay()
96 alice := testutils.TestAddress("alice")
97 checkIn(cur, alice)
98
99 advanceDays(1) // yesterday — still within reach
100 uassert.Equal(t, 1, Current(alice))
101 advanceDays(1) // gap: the run is over
102 uassert.Equal(t, 0, Current(alice))
103 uassert.Equal(t, 1, Best(alice)) // record untouched
104}
105
106func TestStreaksAreIndependentPerAddress(cur realm, t *testing.T) {
107 reset()
108 alignDay()
109 alice := testutils.TestAddress("alice")
110 bob := testutils.TestAddress("bob")
111
112 checkIn(cur, alice)
113 advanceDays(1)
114 checkIn(cur, alice)
115 checkIn(cur, bob)
116
117 uassert.Equal(t, 2, Current(alice))
118 uassert.Equal(t, 1, Current(bob))
119}
120
121func TestUnknownAddressIsZero(t *testing.T) {
122 reset()
123 carol := testutils.TestAddress("carol")
124 uassert.Equal(t, 0, Current(carol))
125 uassert.Equal(t, 0, Best(carol))
126}
127
128func TestRenderEmptyBoard(t *testing.T) {
129 reset()
130 out := Render("")
131 uassert.True(t, strings.Contains(out, "Nobody has checked in yet"))
132 uassert.True(t, strings.Contains(out, "Current day: **"+strconv.FormatInt(Day(), 10)+"**"))
133}
134
135func TestRenderRanksByBest(cur realm, t *testing.T) {
136 reset()
137 alignDay()
138 alice := testutils.TestAddress("alice")
139 bob := testutils.TestAddress("bob")
140 // alice reaches 2, bob only 1
141 checkIn(cur, alice)
142 advanceDays(1)
143 checkIn(cur, alice)
144 checkIn(cur, bob)
145
146 out := Render("")
147 ai := strings.Index(out, alice.String())
148 bi := strings.Index(out, bob.String())
149 uassert.True(t, ai >= 0)
150 uassert.True(t, bi >= 0)
151 uassert.True(t, ai < bi) // alice ranks first
152}
153
154func TestRenderSingleAddress(cur realm, t *testing.T) {
155 reset()
156 alignDay()
157 alice := testutils.TestAddress("alice")
158 checkIn(cur, alice)
159
160 out := Render("/" + alice.String())
161 uassert.True(t, strings.Contains(out, "current streak: **1**"))
162 uassert.True(t, strings.Contains(out, "total check-ins: **1**"))
163}
164
165func TestRenderRejectsBadAddress(t *testing.T) {
166 reset()
167 out := Render("/not-an-address")
168 uassert.True(t, strings.Contains(out, "Not a valid address"))
169}
170
171// TestRenderOneValidatesBeforeEcho pins the order in renderOne: a path that is
172// not an address is refused without being written back. v0 wrote it into a
173// heading between two backticks first, then checked it.
174func TestRenderOneValidatesBeforeEcho(t *testing.T) {
175 const payload = "x`  `y"
176 got := Render("/" + payload)
177 uassert.True(t, strings.HasSuffix(got, "**\n\n_Not a valid address._\n"), "no refusal")
178 uassert.False(t, strings.Contains(got, "![p]"), "the path was echoed")
179}