closestguess_test.gno
4.13 Kb · 151 lines
1package closestguess
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/testutils/v0"
8)
9
10// resetGame re-initializes package globals so each test starts clean; init()
11// runs only once for the whole test binary.
12func resetGame(height int64) {
13 current = newRound(1, height)
14 entries = nil
15 leaderboard = nil
16}
17
18func TestTargetFromHeight(t *testing.T) {
19 tests := []struct {
20 name string
21 height int64
22 }{
23 {"zero", 0},
24 {"one", 1},
25 {"large", 123456},
26 {"negative", -42},
27 }
28 for _, tt := range tests {
29 got := targetFromHeight(tt.height, 1)
30 if got < minTarget || got > maxTarget {
31 t.Errorf("%s: targetFromHeight(%d,1)=%d, want in %d..%d", tt.name, tt.height, got, minTarget, maxTarget)
32 }
33 if again := targetFromHeight(tt.height, 1); again != got {
34 t.Errorf("%s: non-deterministic: %d != %d", tt.name, again, got)
35 }
36 }
37
38 // Different round numbers at the same height should (almost always) mix
39 // to different targets, so consecutive rounds don't repeat.
40 a := targetFromHeight(100, 1)
41 b := targetFromHeight(100, 2)
42 if a == b {
43 t.Errorf("targetFromHeight(100,1) == targetFromHeight(100,2) == %d, want them to differ", a)
44 }
45}
46
47func TestDistance(t *testing.T) {
48 cases := []struct{ a, b, want int }{
49 {5, 5, 0},
50 {5, 8, 3},
51 {8, 5, 3},
52 {0, 1000, 1000},
53 }
54 for _, c := range cases {
55 if got := distance(c.a, c.b); got != c.want {
56 t.Errorf("distance(%d,%d) = %d, want %d", c.a, c.b, got, c.want)
57 }
58 }
59}
60
61// TestGuessRejectsOutOfRangeAndDuplicate exercises the crossing Guess
62// function, so it takes cur realm and calls through cross(cur). A panic
63// raised across the crossing call boundary isn't catchable by a plain
64// defer/recover; revive() is the boundary-aware way to assert on it.
65func TestGuessRejectsOutOfRangeAndDuplicate(cur realm, t *testing.T) {
66 resetGame(10)
67
68 alice := testutils.TestAddress("alice")
69 testing.SetRealm(testing.NewUserRealm(alice))
70 if r := revive(func() { Guess(cross(cur), 0) }); r == nil {
71 t.Error("Guess(0) should panic: out of range")
72 }
73
74 testing.SetRealm(testing.NewUserRealm(alice))
75 msg := Guess(cross(cur), 500)
76 if !strings.Contains(msg, "recorded") {
77 t.Errorf("Guess(500) = %q, want it to mention 'recorded'", msg)
78 }
79
80 testing.SetRealm(testing.NewUserRealm(alice))
81 if r := revive(func() { Guess(cross(cur), 501) }); r == nil {
82 t.Error("second Guess from the same address should panic")
83 }
84}
85
86// TestRevealPicksClosestAndStartsNewRound exercises Guess + Reveal together.
87func TestRevealPicksClosestAndStartsNewRound(cur realm, t *testing.T) {
88 resetGame(50)
89 target := current.target
90 prevRound := current.number
91
92 alice := testutils.TestAddress("alice")
93 bob := testutils.TestAddress("bob")
94
95 far := target + 100
96 if far > maxTarget {
97 far = target - 100
98 }
99 if far < minTarget {
100 far = minTarget
101 }
102 near := target + 1
103 if near > maxTarget {
104 near = target - 1
105 }
106 if near < minTarget {
107 near = minTarget
108 }
109
110 testing.SetRealm(testing.NewUserRealm(alice))
111 Guess(cross(cur), far)
112 testing.SetRealm(testing.NewUserRealm(bob))
113 Guess(cross(cur), near)
114
115 msg := Reveal(cross(cur))
116 if !strings.Contains(msg, "revealed") {
117 t.Errorf("Reveal() = %q, want it to mention 'revealed'", msg)
118 }
119 if len(leaderboard) != 1 {
120 t.Fatalf("leaderboard len = %d, want 1", len(leaderboard))
121 }
122 if leaderboard[0].winner != bob.String() {
123 t.Errorf("winner = %q, want %q (closer guess)", leaderboard[0].winner, bob.String())
124 }
125 if current.number != prevRound+1 {
126 t.Errorf("round number = %d, want %d", current.number, prevRound+1)
127 }
128 if len(entries) != 0 {
129 t.Errorf("entries should reset after Reveal, got %d", len(entries))
130 }
131}
132
133// TestRevealPanicsWithNoGuesses checks the panic path via revive() (see the
134// note on TestGuessRejectsOutOfRangeAndDuplicate).
135func TestRevealPanicsWithNoGuesses(cur realm, t *testing.T) {
136 resetGame(1)
137 if r := revive(func() { Reveal(cross(cur)) }); r == nil {
138 t.Error("Reveal() with no guesses should panic")
139 }
140}
141
142// TestRenderContainsState is a plain read — no cur.
143func TestRenderContainsState(t *testing.T) {
144 resetGame(3)
145 out := Render("")
146 for _, want := range []string{"Closest Guess", "Current round", "Leaderboard"} {
147 if !strings.Contains(out, want) {
148 t.Errorf("Render() missing %q", want)
149 }
150 }
151}