ballot_test.gno
5.88 Kb · 217 lines
1package ballot
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/avl/v0"
8 "gno.land/p/nt/testutils/v0"
9)
10
11func resetState() {
12 var zero address
13 chairperson = zero
14 initialized = false
15 proposals = nil
16 voters = avl.NewTree()
17}
18
19// Crossing: Init/Vote/Delegate/GiveRightToVote mutate realm state, so tests
20// that call them need `cur realm`.
21func TestInitSetsChairpersonAndProposals(cur realm, t *testing.T) {
22 resetState()
23 alice := testutils.TestAddress("alice")
24
25 testing.SetRealm(testing.NewUserRealm(alice))
26 Init(cross(cur), "Alice, Bob , Carol")
27
28 if chairperson != alice {
29 t.Fatal("chairperson should be the Init caller")
30 }
31 if len(proposals) != 3 {
32 t.Fatalf("len(proposals) = %d, want 3", len(proposals))
33 }
34 if proposals[0].name != "Alice" || proposals[1].name != "Bob" || proposals[2].name != "Carol" {
35 t.Fatal("proposal names should be trimmed and in order")
36 }
37
38 rec := revive(func() { Init(cross(cur), "X") })
39 if rec == nil {
40 t.Fatal("expected panic on double Init")
41 }
42}
43
44func TestGiveRightToVoteOnlyChairperson(cur realm, t *testing.T) {
45 resetState()
46 alice := testutils.TestAddress("alice")
47 bob := testutils.TestAddress("bob")
48 mallory := testutils.TestAddress("mallory")
49
50 testing.SetRealm(testing.NewUserRealm(alice))
51 Init(cross(cur), "A,B")
52
53 testing.SetRealm(testing.NewUserRealm(mallory))
54 rec := revive(func() { GiveRightToVote(cross(cur), bob.String()) })
55 if rec == nil {
56 t.Fatal("expected panic when a non-chairperson grants voting rights")
57 }
58
59 testing.SetRealm(testing.NewUserRealm(alice))
60 GiveRightToVote(cross(cur), bob.String())
61
62 v := voters.Get(bob.String())
63 if v == nil || v.(*voterInfo).weight != 1 {
64 t.Fatal("bob should now have voting weight 1")
65 }
66}
67
68func TestVoteDirectly(cur realm, t *testing.T) {
69 resetState()
70 alice := testutils.TestAddress("alice")
71
72 testing.SetRealm(testing.NewUserRealm(alice))
73 Init(cross(cur), "A,B")
74 Vote(cross(cur), 1)
75
76 if proposals[1].votes != 1 {
77 t.Fatalf("proposals[1].votes = %d, want 1", proposals[1].votes)
78 }
79 if WinnerName() != "B" {
80 t.Fatalf("WinnerName() = %q, want B", WinnerName())
81 }
82
83 rec := revive(func() { Vote(cross(cur), 0) })
84 if rec == nil {
85 t.Fatal("expected panic on double vote")
86 }
87}
88
89func TestDelegateToUnvotedAccumulatesWeight(cur realm, t *testing.T) {
90 resetState()
91 alice := testutils.TestAddress("alice")
92 bob := testutils.TestAddress("bob")
93
94 testing.SetRealm(testing.NewUserRealm(alice))
95 Init(cross(cur), "A,B")
96 GiveRightToVote(cross(cur), bob.String())
97
98 testing.SetRealm(testing.NewUserRealm(bob))
99 Delegate(cross(cur), alice.String())
100
101 aliceVoter := voters.Get(alice.String())
102 if aliceVoter.(*voterInfo).weight != 2 {
103 t.Fatalf("alice.weight = %d, want 2", aliceVoter.(*voterInfo).weight)
104 }
105
106 testing.SetRealm(testing.NewUserRealm(alice))
107 Vote(cross(cur), 0)
108
109 if proposals[0].votes != 2 {
110 t.Fatalf("proposals[0].votes = %d, want 2 (alice's own weight + bob's delegation)", proposals[0].votes)
111 }
112}
113
114func TestDelegateToAlreadyVotedAddsDirectly(cur realm, t *testing.T) {
115 resetState()
116 alice := testutils.TestAddress("alice")
117 bob := testutils.TestAddress("bob")
118
119 testing.SetRealm(testing.NewUserRealm(alice))
120 Init(cross(cur), "A,B")
121 GiveRightToVote(cross(cur), bob.String())
122 Vote(cross(cur), 1)
123
124 testing.SetRealm(testing.NewUserRealm(bob))
125 Delegate(cross(cur), alice.String())
126
127 if proposals[1].votes != 2 {
128 t.Fatalf("proposals[1].votes = %d, want 2 (alice's vote + bob's delegated weight)", proposals[1].votes)
129 }
130}
131
132func TestSelfDelegatePanics(cur realm, t *testing.T) {
133 resetState()
134 alice := testutils.TestAddress("alice")
135
136 testing.SetRealm(testing.NewUserRealm(alice))
137 Init(cross(cur), "A,B")
138
139 rec := revive(func() { Delegate(cross(cur), alice.String()) })
140 if rec == nil {
141 t.Fatal("expected panic on self-delegation")
142 }
143}
144
145func TestDelegateLoopPanics(cur realm, t *testing.T) {
146 resetState()
147 alice := testutils.TestAddress("alice")
148 bob := testutils.TestAddress("bob")
149 carol := testutils.TestAddress("carol")
150
151 testing.SetRealm(testing.NewUserRealm(alice))
152 Init(cross(cur), "A,B")
153 GiveRightToVote(cross(cur), bob.String())
154 GiveRightToVote(cross(cur), carol.String())
155
156 // alice -> bob
157 testing.SetRealm(testing.NewUserRealm(alice))
158 Delegate(cross(cur), bob.String())
159
160 // carol -> alice would close the loop carol -> alice -> bob if bob then
161 // delegated back to carol; instead exercise the direct A -> B -> A cycle:
162 testing.SetRealm(testing.NewUserRealm(carol))
163 rec := revive(func() {
164 // bob has no delegate yet (he's an accumulation point), so seed a
165 // cycle by having carol delegate to bob, then bob delegate to carol.
166 Delegate(cross(cur), bob.String())
167 })
168 if rec != nil {
169 t.Fatal("carol -> bob should succeed (bob hasn't voted or delegated)")
170 }
171
172 testing.SetRealm(testing.NewUserRealm(bob))
173 rec = revive(func() { Delegate(cross(cur), carol.String()) })
174 if rec == nil {
175 t.Fatal("expected panic when closing a delegation loop (bob -> carol -> bob)")
176 }
177}
178
179func TestWinningProposalTieBreaksLowestIndex(cur realm, t *testing.T) {
180 resetState()
181 alice := testutils.TestAddress("alice")
182 bob := testutils.TestAddress("bob")
183
184 testing.SetRealm(testing.NewUserRealm(alice))
185 Init(cross(cur), "A,B,C")
186 GiveRightToVote(cross(cur), bob.String())
187 Vote(cross(cur), 1)
188
189 testing.SetRealm(testing.NewUserRealm(bob))
190 Vote(cross(cur), 0)
191
192 if WinningProposal() != 0 {
193 t.Fatalf("WinningProposal() = %d, want 0 (tie broken toward lowest index)", WinningProposal())
194 }
195}
196
197// Non-crossing: Render is a plain read.
198func TestRenderBeforeInit(t *testing.T) {
199 resetState()
200 if out := Render(""); out == "" {
201 t.Fatal(`Render("") should not be empty before Init`)
202 }
203}
204
205func TestRenderAfterInit(cur realm, t *testing.T) {
206 resetState()
207 alice := testutils.TestAddress("alice")
208
209 testing.SetRealm(testing.NewUserRealm(alice))
210 Init(cross(cur), "A,B")
211 Vote(cross(cur), 0)
212
213 out := Render("")
214 if !strings.Contains(out, "A") || !strings.Contains(out, alice.String()) {
215 t.Fatal("Render should include the proposal names and the chairperson address")
216 }
217}