cond_gnolovedao_test.gno
8.64 Kb ยท 333 lines
1package daocond
2
3import (
4 "testing"
5
6 "gno.land/p/nt/ufmt"
7 "gno.land/p/nt/urequire"
8)
9
10/*
11Example 1:
12T1 100 members --> 300 VP, 3 votes per member
13T2 100 members --> 200 VP, 2 votes per member
14T3 100 members --> 100 VP, 1 votes per member
15Example 2:
16
17T1 100 members --> 300 VP, 3 votes per member
18T2 50 members --> 100 VP, 2 votes per member *
19T3 10 members --> 10 VP, 1 votes per member *
20Example 3:
21
22T1 100 members --> 300 VP, 3 votes per member
23T2 200 members --> 200 VP, 1 votes per member *
24T3 100 members --> 100 VP, 1 votes per member
25Example 4:
26
27T1 100 members --> 300 VP, 3 votes per member
28T2 200 members --> 200 VP, 1 votes per member *
29T3 1000 members --> 100 VP, 0.1 votes per member
30*/
31func TestComputeVotingPowers(t *testing.T) {
32 type gnoloveDaoComposition struct {
33 t1s int
34 t2s int
35 t3s int
36 abstainT3 bool
37 expectedTotalPower float64
38 expectedPowers map[string]float64
39 }
40 tests := map[string]gnoloveDaoComposition{
41 "example 1": {
42 t1s: 100,
43 t2s: 100,
44 t3s: 100,
45 abstainT3: false,
46 expectedTotalPower: 600,
47 expectedPowers: map[string]float64{
48 "T1": 3.0,
49 "T2": 2.0,
50 "T3": 1.0,
51 },
52 },
53 "example 2": {
54 t1s: 100,
55 t2s: 50,
56 t3s: 10,
57 abstainT3: false,
58 expectedTotalPower: 410,
59 expectedPowers: map[string]float64{
60 "T1": 3.0,
61 "T2": 2.0,
62 "T3": 1.0,
63 },
64 },
65 "example 3": {
66 t1s: 100,
67 t2s: 200,
68 t3s: 100,
69 abstainT3: false,
70 expectedTotalPower: 600,
71 expectedPowers: map[string]float64{
72 "T1": 3.0,
73 "T2": 1.0,
74 "T3": 1.0,
75 },
76 },
77 "example 4": {
78 t1s: 100,
79 t2s: 200,
80 t3s: 1000,
81 abstainT3: false,
82 expectedTotalPower: 600,
83 expectedPowers: map[string]float64{
84 "T1": 3.0,
85 "T2": 1.0,
86 "T3": 0.1,
87 },
88 },
89 "0 -T1s": {
90 t1s: 0,
91 t2s: 100,
92 t3s: 100,
93 abstainT3: false,
94 expectedTotalPower: 0,
95 expectedPowers: map[string]float64{
96 "T1": 3.0,
97 "T2": 0.0,
98 "T3": 0.0,
99 },
100 },
101 "100 T1, 1 T2, 1 T3": {
102 t1s: 100,
103 t2s: 1,
104 t3s: 1,
105 abstainT3: false,
106 expectedTotalPower: 303,
107 expectedPowers: map[string]float64{
108 "T1": 3.0,
109 "T2": 2.0,
110 "T3": 1.0,
111 },
112 },
113 "T3 Abstaining": {
114 t1s: 100,
115 t2s: 100,
116 t3s: 100,
117 expectedTotalPower: 500,
118 abstainT3: true,
119 expectedPowers: map[string]float64{
120 "T1": 3.0,
121 "T2": 2.0,
122 },
123 },
124 }
125
126 for name, composition := range tests {
127 t.Run(name, func(t *testing.T) {
128 dao := newMockDAO()
129 for i := 0; i < composition.t1s; i++ {
130 dao.addUser(ufmt.Sprintf("%d_T1", i), []string{"T1"})
131 }
132 for i := 0; i < composition.t2s; i++ {
133 dao.addUser(ufmt.Sprintf("%d_T2", i), []string{"T2"})
134 }
135 for i := 0; i < composition.t3s; i++ {
136 dao.addUser(ufmt.Sprintf("%d_T3", i), []string{"T3"})
137 }
138
139 roles := []string{"T1", "T2"}
140 if !composition.abstainT3 {
141 roles = append(roles, "T3")
142 }
143
144 cond := &gnolovDaoCondThreshold{
145 threshold: 0.6,
146 hasRoleFn: dao.hasRole,
147 roles: roles,
148 usersWithRoleCountFn: dao.usersWithRoleCount,
149 }
150 votingPowers, totalPower := cond.computeVotingPowers()
151 for tier, expectedPower := range composition.expectedPowers {
152 if votingPowers[tier] != expectedPower {
153 t.Fail()
154 }
155 }
156
157 if totalPower != composition.expectedTotalPower {
158 t.Fail()
159 }
160 })
161 }
162}
163
164func TestEval(t *testing.T) {
165 type gnoloveDaoVotes struct {
166 votesT1 []Vote
167 votesT2 []Vote
168 votesT3 []Vote
169 threshold float64
170 expectedEval bool
171 expectedYes float64
172 abstainT3 bool
173 panic bool
174 }
175 tests := map[string]gnoloveDaoVotes{
176 "2/6% Yes": { //0.3333
177 votesT1: []Vote{VoteNo}, // 3 voting power
178 votesT2: []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // 2 voting power combined
179 votesT3: []Vote{VoteNo},
180 expectedEval: false,
181 abstainT3: false,
182 threshold: 0.45,
183 expectedYes: 2.0 / 6.0,
184 panic: false,
185 },
186 "50% Yes": {
187 votesT1: []Vote{VoteNo}, // 3 voting power
188 votesT2: []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // 2 voting power combined
189 votesT3: []Vote{VoteYes},
190 expectedEval: true,
191 abstainT3: false,
192 threshold: 0.45,
193 expectedYes: 3.0 / 6.0,
194 panic: false,
195 },
196 "several T2 & T3": {
197 votesT1: []Vote{VoteNo, VoteNo}, // 6 voting power
198 // 10 T2 total power (2/3) powerT1 = 4, 4/10 0.4 each
199 votesT2: []Vote{VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes},
200 // 4 T3 total power (1/3) powerT1 = 2, 2/4 0.5 each
201 votesT3: []Vote{VoteYes, VoteNo, VoteYes, VoteNo},
202 expectedEval: false,
203 abstainT3: false,
204 threshold: 0.42, //total power = 6+4+2 T3yes = 1, T2yes = 4 T1yes = 0 totalYes = 0.41666666666 (5/12)
205 expectedYes: 5.0 / 12.0,
206 panic: false,
207 },
208 "several T2 & T3 eval true": {
209 votesT1: []Vote{VoteNo, VoteNo}, // 6 voting power
210 // 10 T2 total power (2/3) powerT1 = 4, 4/10 0.4 each
211 votesT2: []Vote{VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes, VoteYes},
212 // 4 T3 total power (1/3) powerT1 = 2, 2/4 0.5 each
213 votesT3: []Vote{VoteYes, VoteYes, VoteYes, VoteNo},
214 expectedEval: true,
215 abstainT3: false,
216 threshold: 0.42, //total power = 6+4+2 T3yes = 1.5, T2yes = 4 T1yes = 0 totalYes = 0.45833333333 (5.5/12)
217 expectedYes: 5.5 / 12.0,
218 panic: false,
219 },
220 "only T1s": {
221 votesT1: []Vote{VoteYes, VoteNo}, // 6 voting power
222 expectedEval: false,
223 abstainT3: false,
224 threshold: 0.6,
225 expectedYes: 3.0 / 6.0,
226 panic: false,
227 },
228 "only T3s": { // as T2 & T3 power is capped as power of T1, in this case the power will be 0 everywhere
229 votesT3: []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // voting power = 0, 0 each
230 expectedEval: false,
231 abstainT3: false,
232 threshold: 0.6,
233 expectedYes: 0.0,
234 },
235 "T3 abstaining": {
236 votesT1: []Vote{VoteYes, VoteNo}, // 6 voting power
237 votesT2: []Vote{VoteYes, VoteYes},
238 votesT3: []Vote{},
239 expectedEval: true,
240 abstainT3: true,
241 threshold: 0.6,
242 expectedYes: 7.0 / 10.0,
243 panic: false,
244 },
245 "T3 abstaining w/ votes": {
246 votesT1: []Vote{VoteYes, VoteNo}, // 6 voting power
247 votesT2: []Vote{VoteYes, VoteYes},
248 votesT3: []Vote{VoteYes, VoteNo},
249 expectedEval: true,
250 abstainT3: true,
251 threshold: 0.6,
252 expectedYes: 7.0 / 10.0,
253 panic: true, // a user with T3 when T3 is abstaining should panic
254 },
255 }
256
257 for name, tdata := range tests {
258 t.Run(name, func(t *testing.T) {
259 if tdata.panic {
260 defer func() {
261 if r := recover(); r == nil {
262 t.Errorf("The code did not panic")
263 }
264 }()
265 }
266 votes := map[string]Vote{}
267 dao := newMockDAO()
268 for i, vote := range tdata.votesT1 {
269 userID := ufmt.Sprintf("%d_T1", i)
270 dao.addUser(userID, []string{"T1"})
271 votes[userID] = vote
272 }
273
274 for i, vote := range tdata.votesT2 {
275 userID := ufmt.Sprintf("%d_T2", i)
276 dao.addUser(userID, []string{"T2"})
277 votes[userID] = vote
278 }
279
280 for i, vote := range tdata.votesT3 {
281 userID := ufmt.Sprintf("%d_T3", i)
282 dao.addUser(userID, []string{"T3"})
283 votes[userID] = vote
284 }
285 ballot := NewBallot()
286 for userId, vote := range votes {
287 ballot.Vote(userId, vote)
288 }
289 roles := []string{"T1", "T2"}
290 if !tdata.abstainT3 {
291 roles = append(roles, "T3")
292 }
293 cond := GnoloveDAOCondThreshold(tdata.threshold, roles, dao.hasRole, dao.usersWithRoleCount)
294 // Get percent of total yes
295 urequire.Equal(t, tdata.expectedEval, cond.Eval(ballot))
296 })
297 }
298}
299
300type mockDAO struct {
301 members map[string][]string
302 roles map[string][]string
303}
304
305func newMockDAO() *mockDAO {
306 return &mockDAO{
307 members: map[string][]string{},
308 roles: map[string][]string{}, // roles to users
309 }
310}
311
312func (m *mockDAO) addUser(memberId string, roles []string) {
313 m.members[memberId] = roles
314 for _, memberRole := range roles {
315 m.roles[memberRole] = append(m.roles[memberRole], memberId)
316 }
317}
318func (m *mockDAO) usersWithRoleCount(role string) uint32 {
319 return uint32(len(m.roles[role]))
320}
321
322func (m *mockDAO) hasRole(memberId string, role string) bool {
323 roles, ok := m.members[memberId]
324 if !ok {
325 return false
326 }
327 for _, memberRole := range roles {
328 if memberRole == role {
329 return true
330 }
331 }
332 return false
333}